diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml
index 8c2b28d37..b9df4030d 100644
--- a/.github/workflows/enforce_branch_name.yml
+++ b/.github/workflows/enforce_branch_name.yml
@@ -4,17 +4,23 @@ on:
pull_request_target:
types: [opened, reopened, synchronize]
+permissions:
+ issues: write
+
jobs:
check-source-branch:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check PR source branch
+ env:
+ IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
+ HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
# Check if PR is from a fork
- if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
+ if [[ "$IS_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
+ if [[ "$HEAD_REF" == "master" || "$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
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 9018d251b..2122e5b99 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -94,7 +94,7 @@ jobs:
- name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0
with:
- version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
+ version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
- run: ./scripts/wasm_bindgen_check.sh --skip-setup
diff --git a/.typos.toml b/.typos.toml
index 16659f4c7..97c54d657 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -9,6 +9,7 @@ isse = "isse" # part of @IsseW username
tye = "tye" # part of @tye-exe username
ro = "ro" # read-only, also part of the username @Phen-Ro
typ = "typ" # Often used because `type` is a keyword in Rust
+wdth = "wdth" # The `wdth` tag is used in variable fonts
# I mistype these so often
tesalator = "tessellator"
@@ -20,6 +21,8 @@ teselation = "tessellation"
tessalation = "tessellation"
tesselation = "tessellation"
+# For consistency
+postfix = "suffix"
# Use the more common spelling
adaptor = "adapter"
diff --git a/Cargo.lock b/Cargo.lock
index d86175c25..37180245f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,47 +20,48 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "accesskit"
-version = "0.21.1"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99"
+checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a"
dependencies = [
"enumn",
"serde",
+ "uuid",
]
[[package]]
name = "accesskit_atspi_common"
-version = "0.14.1"
+version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29f73a9b855b6f4af4962a94553ef0c092b80cf5e17038724d5e30945d036f69"
+checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260"
dependencies = [
"accesskit",
"accesskit_consumer",
"atspi-common",
+ "phf 0.13.1",
"serde",
- "thiserror 1.0.66",
"zvariant",
]
[[package]]
name = "accesskit_consumer"
-version = "0.30.1"
+version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdd06f5fea9819250fffd4debf926709f3593ac22f8c1541a2573e5ee0ca01cd"
+checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
dependencies = [
"accesskit",
- "hashbrown 0.15.2",
+ "hashbrown 0.16.1",
]
[[package]]
name = "accesskit_macos"
-version = "0.22.1"
+version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fbaf15815f39084e0cb24950c232f0e3634702c2dfbf182ae3b4919a4a1d45"
+checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243"
dependencies = [
"accesskit",
"accesskit_consumer",
- "hashbrown 0.15.2",
+ "hashbrown 0.16.1",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -68,9 +69,9 @@ dependencies = [
[[package]]
name = "accesskit_unix"
-version = "0.17.1"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64926a930368d52d95422b822ede15014c04536cabaa2394f99567a1f4788dc6"
+checksum = "90e549dd7c6562b6a2ea807b44726e6241707db054a817dc4c7e2b8d3b39bfac"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -86,23 +87,23 @@ dependencies = [
[[package]]
name = "accesskit_windows"
-version = "0.29.1"
+version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "792991159fa9ba57459de59e12e918bb90c5346fea7d40ac1a11f8632b41e63a"
+checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
dependencies = [
"accesskit",
"accesskit_consumer",
- "hashbrown 0.15.2",
+ "hashbrown 0.16.1",
"static_assertions",
- "windows 0.61.1",
- "windows-core 0.61.0",
+ "windows",
+ "windows-core",
]
[[package]]
name = "accesskit_winit"
-version = "0.29.1"
+version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd9db0ea66997e3f4eae4a5f2c6b6486cf206642639ee629dbbb860ace1dec87"
+checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
dependencies = [
"accesskit",
"accesskit_macos",
@@ -123,9 +124,9 @@ dependencies = [
[[package]]
name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
@@ -138,7 +139,7 @@ dependencies = [
"once_cell",
"serde",
"version_check",
- "zerocopy 0.8.27",
+ "zerocopy",
]
[[package]]
@@ -150,6 +151,12 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
[[package]]
name = "android-activity"
version = "0.6.0"
@@ -274,7 +281,7 @@ dependencies = [
"clipboard-win",
"image",
"log",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-core-graphics",
@@ -312,28 +319,6 @@ dependencies = [
"libloading",
]
-[[package]]
-name = "ashpd"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
-dependencies = [
- "async-fs",
- "async-net",
- "enumflags2",
- "futures-channel",
- "futures-util",
- "rand 0.9.2",
- "raw-window-handle",
- "serde",
- "serde_repr",
- "url",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols",
- "zbus",
-]
-
[[package]]
name = "async-broadcast"
version = "0.7.1"
@@ -371,17 +356,6 @@ dependencies = [
"slab",
]
-[[package]]
-name = "async-fs"
-version = "2.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
-dependencies = [
- "async-lock",
- "blocking",
- "futures-lite",
-]
-
[[package]]
name = "async-io"
version = "2.3.4"
@@ -412,17 +386,6 @@ dependencies = [
"pin-project-lite",
]
-[[package]]
-name = "async-net"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
-dependencies = [
- "async-io",
- "blocking",
- "futures-lite",
-]
-
[[package]]
name = "async-process"
version = "2.3.0"
@@ -496,20 +459,19 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atspi"
-version = "0.25.0"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c"
+checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d"
dependencies = [
"atspi-common",
- "atspi-connection",
"atspi-proxies",
]
[[package]]
name = "atspi-common"
-version = "0.9.0"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb"
+checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d"
dependencies = [
"enumflags2",
"serde",
@@ -521,23 +483,11 @@ dependencies = [
"zvariant",
]
-[[package]]
-name = "atspi-connection"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938"
-dependencies = [
- "atspi-common",
- "atspi-proxies",
- "futures-lite",
- "zbus",
-]
-
[[package]]
name = "atspi-proxies"
-version = "0.9.0"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c"
+checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc"
dependencies = [
"atspi-common",
"serde",
@@ -546,9 +496,9 @@ dependencies = [
[[package]]
name = "autocfg"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
@@ -586,7 +536,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
- "bit-vec",
+ "bit-vec 0.8.0",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd"
+dependencies = [
+ "bit-vec 0.9.1",
]
[[package]]
@@ -595,6 +554,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -610,12 +575,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "block"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
-
[[package]]
name = "block2"
version = "0.5.1"
@@ -631,7 +590,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
- "objc2 0.6.3",
+ "objc2 0.6.4",
]
[[package]]
@@ -660,9 +619,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.16.0"
+version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytemuck"
@@ -736,10 +695,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.16"
+version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
+checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
+ "find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -753,9 +713,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
-version = "1.0.0"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
@@ -772,19 +732,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "chrono"
-version = "0.4.42"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link 0.2.1",
-]
-
[[package]]
name = "ciborium"
version = "0.2.2"
@@ -854,9 +801,9 @@ dependencies = [
[[package]]
name = "codespan-reporting"
-version = "0.12.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
+checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
@@ -973,7 +920,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
- "core-graphics-types 0.1.3",
+ "core-graphics-types",
"foreign-types",
"libc",
]
@@ -989,17 +936,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "core-graphics-types"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
-dependencies = [
- "bitflags 2.9.4",
- "core-foundation 0.10.1",
- "libc",
-]
-
[[package]]
name = "core_maths"
version = "0.1.1"
@@ -1011,9 +947,9 @@ dependencies = [
[[package]]
name = "crc32fast"
-version = "1.4.2"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@@ -1164,9 +1100,9 @@ dependencies = [
[[package]]
name = "dify"
-version = "0.7.4"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6"
+checksum = "90ce0fb972943b4e88cd03b8f92953df0c71bb05e0bde8e5b684895d808013cc"
dependencies = [
"anyhow",
"colored",
@@ -1211,7 +1147,7 @@ dependencies = [
"bitflags 2.9.4",
"block2 0.6.2",
"libc",
- "objc2 0.6.3",
+ "objc2 0.6.4",
]
[[package]]
@@ -1236,9 +1172,9 @@ dependencies = [
[[package]]
name = "document-features"
-version = "0.2.11"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
@@ -1286,9 +1222,9 @@ dependencies = [
"image",
"js-sys",
"log",
- "objc2 0.5.2",
- "objc2-app-kit 0.2.2",
- "objc2-foundation 0.2.2",
+ "objc2 0.6.4",
+ "objc2-app-kit 0.3.2",
+ "objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"pollster",
@@ -1354,9 +1290,9 @@ dependencies = [
"document-features",
"egui",
"log",
- "objc2 0.5.2",
- "objc2-foundation 0.2.2",
- "objc2-ui-kit",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+ "objc2-ui-kit 0.3.2",
"profiling",
"raw-window-handle",
"serde",
@@ -1374,7 +1310,6 @@ dependencies = [
"accesskit",
"accesskit_consumer",
"bytemuck",
- "chrono",
"eframe",
"egui",
"egui_demo_lib",
@@ -1383,6 +1318,7 @@ dependencies = [
"ehttp",
"env_logger",
"image",
+ "jiff",
"log",
"mimalloc",
"poll-promise",
@@ -1401,13 +1337,13 @@ dependencies = [
name = "egui_demo_lib"
version = "0.33.3"
dependencies = [
- "chrono",
"criterion",
"document-features",
"egui",
"egui_extras",
"egui_kittest",
"image",
+ "jiff",
"mimalloc",
"rand 0.9.2",
"serde",
@@ -1419,12 +1355,12 @@ name = "egui_extras"
version = "0.33.3"
dependencies = [
"ahash",
- "chrono",
"document-features",
"egui",
"ehttp",
"enum-map",
"image",
+ "jiff",
"log",
"mime_guess2",
"profiling",
@@ -1484,9 +1420,9 @@ dependencies = [
[[package]]
name = "ehttp"
-version = "0.6.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04499d3c719edecfad5c9b46031726c8540905d73be6d7e4f9788c4a298da908"
+checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc"
dependencies = [
"document-features",
"js-sys",
@@ -1611,6 +1547,7 @@ dependencies = [
"ecolor",
"emath",
"epaint_default_fonts",
+ "font-types",
"log",
"mimalloc",
"nohash-hasher",
@@ -1621,6 +1558,7 @@ dependencies = [
"serde",
"similar-asserts",
"skrifa",
+ "smallvec",
"vello_cpu",
"wasm-bindgen",
"web-sys",
@@ -1708,7 +1646,7 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
- "bit-set",
+ "bit-set 0.8.0",
"regex-automata",
"regex-syntax",
]
@@ -1747,10 +1685,16 @@ dependencies = [
]
[[package]]
-name = "flate2"
-version = "1.1.4"
+name = "find-msvc-tools"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1782,11 +1726,12 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
-version = "0.10.1"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
+checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23"
dependencies = [
"bytemuck",
+ "serde",
]
[[package]]
@@ -1847,32 +1792,23 @@ dependencies = [
"percent-encoding",
]
-[[package]]
-name = "futures-channel"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
-dependencies = [
- "futures-core",
-]
-
[[package]]
name = "futures-core"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-io"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
-version = "2.6.0"
+version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
@@ -1883,9 +1819,9 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@@ -1894,23 +1830,20 @@ dependencies = [
[[package]]
name = "futures-task"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
-version = "0.3.31"
+version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
- "futures-io",
"futures-macro",
"futures-task",
- "memchr",
"pin-project-lite",
- "pin-utils",
"slab",
]
@@ -1935,13 +1868,13 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.16"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
@@ -1985,9 +1918,9 @@ dependencies = [
[[package]]
name = "glow"
-version = "0.16.0"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
+checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5"
dependencies = [
"js-sys",
"slotmap",
@@ -2009,7 +1942,7 @@ dependencies = [
"glutin_glx_sys",
"glutin_wgl_sys",
"libloading",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
@@ -2061,35 +1994,18 @@ dependencies = [
"gl_generator",
]
-[[package]]
-name = "gpu-alloc"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
-dependencies = [
- "bitflags 2.9.4",
- "gpu-alloc-types",
-]
-
-[[package]]
-name = "gpu-alloc-types"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
-dependencies = [
- "bitflags 2.9.4",
-]
-
[[package]]
name = "gpu-allocator"
-version = "0.27.0"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
+checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795"
dependencies = [
+ "ash",
+ "hashbrown 0.16.1",
"log",
"presser",
- "thiserror 1.0.66",
- "windows 0.58.0",
+ "thiserror 2.0.17",
+ "windows",
]
[[package]]
@@ -2134,10 +2050,12 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.16.0"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
+ "allocator-api2",
+ "equivalent",
"foldhash 0.2.0",
]
@@ -2207,28 +2125,20 @@ dependencies = [
]
[[package]]
-name = "iana-time-zone"
-version = "0.1.63"
+name = "http"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core 0.61.0",
+ "bytes",
+ "itoa",
]
[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
+name = "httparse"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
@@ -2383,23 +2293,12 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
name = "indexmap"
-version = "2.8.0"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
- "hashbrown 0.15.2",
-]
-
-[[package]]
-name = "io-uring"
-version = "0.7.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
-dependencies = [
- "bitflags 2.9.4",
- "cfg-if",
- "libc",
+ "hashbrown 0.16.1",
]
[[package]]
@@ -2447,28 +2346,31 @@ dependencies = [
[[package]]
name = "itoa"
-version = "1.0.11"
+version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
-version = "0.2.15"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
+checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
+ "js-sys",
"log",
"portable-atomic",
"portable-atomic-util",
- "serde",
+ "serde_core",
+ "wasm-bindgen",
+ "windows-sys 0.61.2",
]
[[package]]
name = "jiff-static"
-version = "0.2.15"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
+checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
@@ -2514,9 +2416,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
[[package]]
name = "js-sys"
-version = "0.3.77"
+version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -2549,13 +2451,12 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kittest"
-version = "0.3.0"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01fd6dd2cce251a360101038acb9334e3a50cd38cd02fefddbf28aa975f043c8"
+checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5"
dependencies = [
"accesskit",
"accesskit_consumer",
- "parking_lot",
]
[[package]]
@@ -2570,9 +2471,9 @@ dependencies = [
[[package]]
name = "kurbo"
-version = "0.12.0"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32"
+checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb"
dependencies = [
"arrayvec",
"euclid",
@@ -2587,9 +2488,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.176"
+version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libloading"
@@ -2660,9 +2561,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
-version = "0.4.1"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
@@ -2675,24 +2576,15 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.28"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lz4_flex"
-version = "0.11.3"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
-
-[[package]]
-name = "malloc_buf"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
-dependencies = [
- "libc",
-]
+checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a"
[[package]]
name = "memchr"
@@ -2718,21 +2610,6 @@ dependencies = [
"autocfg",
]
-[[package]]
-name = "metal"
-version = "0.32.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605"
-dependencies = [
- "bitflags 2.9.4",
- "block",
- "core-graphics-types 0.2.0",
- "foreign-types",
- "log",
- "objc",
- "paste",
-]
-
[[package]]
name = "mimalloc"
version = "0.1.48"
@@ -2755,8 +2632,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
dependencies = [
"mime",
- "phf",
- "phf_shared",
+ "phf 0.11.3",
+ "phf_shared 0.11.3",
"unicase",
]
@@ -2783,7 +2660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2797,18 +2674,18 @@ dependencies = [
[[package]]
name = "naga"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12b2e757b11b47345d44e7760e45458339bc490463d9548cd8651c53ae523153"
+checksum = "85b4372fed0bd362d646d01b6926df0e837859ccc522fed720c395e0460f29c8"
dependencies = [
"arrayvec",
- "bit-set",
+ "bit-set 0.9.1",
"bitflags 2.9.4",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
"half",
- "hashbrown 0.16.0",
+ "hashbrown 0.16.1",
"hexf-parse",
"indexmap",
"libm",
@@ -2853,9 +2730,9 @@ dependencies = [
[[package]]
name = "nix"
-version = "0.29.0"
+version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
@@ -2907,15 +2784,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "objc"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
-dependencies = [
- "malloc_buf",
-]
-
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -2934,9 +2802,9 @@ dependencies = [
[[package]]
name = "objc2"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
@@ -2954,7 +2822,7 @@ dependencies = [
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
- "objc2-quartz-core",
+ "objc2-quartz-core 0.2.2",
]
[[package]]
@@ -2965,7 +2833,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.9.4",
"block2 0.6.2",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
@@ -3015,7 +2883,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.9.4",
"dispatch2",
- "objc2 0.6.3",
+ "objc2 0.6.4",
]
[[package]]
@@ -3026,7 +2894,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.9.4",
"dispatch2",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-core-foundation",
"objc2-io-surface",
]
@@ -3040,7 +2908,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
- "objc2-metal",
+ "objc2-metal 0.2.2",
]
[[package]]
@@ -3081,7 +2949,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.9.4",
- "objc2 0.6.3",
+ "block2 0.6.2",
+ "objc2 0.6.4",
"objc2-core-foundation",
]
@@ -3092,7 +2961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.9.4",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-core-foundation",
]
@@ -3120,6 +2989,18 @@ dependencies = [
"objc2-foundation 0.2.2",
]
+[[package]]
+name = "objc2-metal"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
+dependencies = [
+ "bitflags 2.9.4",
+ "block2 0.6.2",
+ "objc2 0.6.4",
+ "objc2-foundation 0.3.2",
+]
+
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
@@ -3130,7 +3011,20 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
- "objc2-metal",
+ "objc2-metal 0.2.2",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.9.4",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
]
[[package]]
@@ -3158,12 +3052,24 @@ dependencies = [
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
- "objc2-quartz-core",
+ "objc2-quartz-core 0.2.2",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
]
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.9.4",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+]
+
[[package]]
name = "objc2-uniform-type-identifiers"
version = "0.2.2"
@@ -3199,9 +3105,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.21.3"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
@@ -3298,12 +3204,6 @@ dependencies = [
"windows-link 0.2.1",
]
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
[[package]]
name = "pathdiff"
version = "0.2.3"
@@ -3312,13 +3212,13 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "peniko"
-version = "0.5.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3c76095c9a636173600478e0373218c7b955335048c2bcd12dc6a79657649d8"
+checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a"
dependencies = [
"bytemuck",
"color",
- "kurbo 0.12.0",
+ "kurbo 0.13.0",
"linebender_resource_handle",
"smallvec",
]
@@ -3335,8 +3235,19 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
- "phf_macros",
- "phf_shared",
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
]
[[package]]
@@ -3345,8 +3256,8 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
- "phf_generator",
- "phf_shared",
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
]
[[package]]
@@ -3355,24 +3266,47 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
- "phf_shared",
+ "phf_shared 0.11.3",
"rand 0.8.5",
]
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared 0.13.1",
+]
+
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
- "phf_generator",
- "phf_shared",
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn",
"unicase",
]
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "phf_shared"
version = "0.11.3"
@@ -3383,6 +3317,15 @@ dependencies = [
"unicase",
]
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
[[package]]
name = "pico-args"
version = "0.5.0"
@@ -3411,15 +3354,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
@@ -3440,13 +3377,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "plist"
-version = "1.7.0"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
+checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64",
"indexmap",
- "quick-xml 0.32.0",
+ "quick-xml",
"serde",
"time",
]
@@ -3535,11 +3472,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
-version = "0.2.20"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
- "zerocopy 0.7.35",
+ "zerocopy",
]
[[package]]
@@ -3550,18 +3487,18 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
[[package]]
name = "proc-macro-crate"
-version = "3.2.0"
+version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
-version = "1.0.101"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -3636,37 +3573,19 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
-version = "0.32.0"
+version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "quick-xml"
-version = "0.36.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
"serde",
]
-[[package]]
-name = "quick-xml"
-version = "0.37.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "quote"
-version = "1.0.41"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -3718,7 +3637,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom 0.2.16",
+ "getrandom 0.2.17",
]
[[package]]
@@ -3742,6 +3661,18 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+[[package]]
+name = "raw-window-metal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
+dependencies = [
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-quartz-core 0.3.2",
+]
+
[[package]]
name = "rayon"
version = "1.11.0"
@@ -3764,9 +3695,9 @@ dependencies = [
[[package]]
name = "read-fonts"
-version = "0.35.0"
+version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"
+checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
dependencies = [
"bytemuck",
"font-types",
@@ -3796,7 +3727,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
- "getrandom 0.2.16",
+ "getrandom 0.2.17",
"libredox",
"thiserror 2.0.17",
]
@@ -3852,26 +3783,29 @@ dependencies = [
[[package]]
name = "rfd"
-version = "0.15.4"
+version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
+checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
- "ashpd",
"block2 0.6.2",
"dispatch2",
"js-sys",
+ "libc",
"log",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
+ "percent-encoding",
"pollster",
"raw-window-handle",
- "urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
"web-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3891,7 +3825,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
- "getrandom 0.2.16",
+ "getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
@@ -3899,14 +3833,15 @@ dependencies = [
[[package]]
name = "ron"
-version = "0.11.0"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468"
+checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
dependencies = [
- "base64",
"bitflags 2.9.4",
+ "once_cell",
"serde",
"serde_derive",
+ "typeid",
"unicode-ident",
]
@@ -3962,9 +3897,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.18"
+version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"log",
"once_cell",
@@ -3977,15 +3912,18 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.10.0"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "zeroize",
+]
[[package]]
name = "rustls-webpki"
-version = "0.102.8"
+version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
+checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"ring",
"rustls-pki-types",
@@ -3994,9 +3932,9 @@ dependencies = [
[[package]]
name = "rustversion"
-version = "1.0.19"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rustybuzz"
@@ -4016,12 +3954,6 @@ dependencies = [
"unicode-script",
]
-[[package]]
-name = "ryu"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
-
[[package]]
name = "same-file"
version = "1.0.6"
@@ -4103,14 +4035,15 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.132"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
- "ryu",
"serde",
+ "serde_core",
+ "zmij",
]
[[package]]
@@ -4126,11 +4059,11 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "0.6.9"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
- "serde",
+ "serde_core",
]
[[package]]
@@ -4159,9 +4092,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
-version = "0.3.7"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "similar"
@@ -4200,9 +4133,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
-version = "0.37.0"
+version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841"
+checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
dependencies = [
"bytemuck",
"read-fonts",
@@ -4210,12 +4143,9 @@ dependencies = [
[[package]]
name = "slab"
-version = "0.4.9"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slotmap"
@@ -4231,6 +4161,9 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
[[package]]
name = "smithay-client-toolkit"
@@ -4289,9 +4222,9 @@ dependencies = [
[[package]]
name = "spirv"
-version = "0.3.0+sdk-1.3.268.0"
+version = "0.4.0+sdk-1.4.341.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
+checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f"
dependencies = [
"bitflags 2.9.4",
]
@@ -4335,9 +4268,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.106"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -4398,6 +4331,14 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "test_background_logic"
+version = "0.1.0"
+dependencies = [
+ "eframe",
+ "env_logger",
+]
+
[[package]]
name = "test_egui_extras_compilation"
version = "0.1.0"
@@ -4584,51 +4525,66 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.47.1"
+version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
- "backtrace",
- "io-uring",
"libc",
"mio",
"pin-project-lite",
- "slab",
"socket2",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "toml"
-version = "0.8.20"
+version = "1.0.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+checksum = "d1dfefef6a142e93f346b64c160934eb13b5594b84ab378133ac6815cb2bd57f"
dependencies = [
- "serde",
+ "serde_core",
"serde_spanned",
- "toml_datetime",
- "toml_edit",
+ "toml_datetime 1.0.0+spec-1.1.0",
+ "toml_parser",
+ "winnow",
]
[[package]]
name = "toml_datetime"
-version = "0.6.8"
+version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
- "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.0.0+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
+dependencies = [
+ "serde_core",
]
[[package]]
name = "toml_edit"
-version = "0.22.24"
+version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
- "serde",
- "serde_spanned",
- "toml_datetime",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.9+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
+dependencies = [
"winnow",
]
@@ -4681,6 +4637,12 @@ dependencies = [
"rustc-hash 2.1.1",
]
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
[[package]]
name = "uds_windows"
version = "1.1.0"
@@ -4718,9 +4680,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-ident"
-version = "1.0.19"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-properties"
@@ -4758,7 +4720,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0"
dependencies = [
- "phf",
+ "phf 0.11.3",
"unicode_names2_generator",
]
@@ -4780,20 +4742,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
-version = "2.10.1"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a"
+checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [
"base64",
"flate2",
"log",
- "once_cell",
+ "percent-encoding",
"rustls",
"rustls-pki-types",
- "url",
+ "ureq-proto",
+ "utf8-zero",
"webpki-roots",
]
+[[package]]
+name = "ureq-proto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
[[package]]
name = "url"
version = "2.5.4"
@@ -4803,15 +4778,8 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
- "serde",
]
-[[package]]
-name = "urlencoding"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
-
[[package]]
name = "user_attention"
version = "0.1.0"
@@ -4847,6 +4815,12 @@ dependencies = [
"xmlwriter",
]
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -4860,13 +4834,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
-name = "vello_common"
-version = "0.0.4"
+name = "uuid"
+version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a235ba928b3109ad9e7696270edb09445a52ae1c7c08e6d31a19b1cdd6cbc24a"
+checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vello_common"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b"
dependencies = [
"bytemuck",
"fearless_simd",
+ "hashbrown 0.16.1",
"log",
"peniko",
"skrifa",
@@ -4875,11 +4861,12 @@ dependencies = [
[[package]]
name = "vello_cpu"
-version = "0.0.4"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0bd1fcf9c1814f17a491e07113623d44e3ec1125a9f3401f5e047d6d326da21"
+checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc"
dependencies = [
"bytemuck",
+ "hashbrown 0.16.1",
"vello_common",
]
@@ -4901,9 +4888,9 @@ dependencies = [
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@@ -4916,37 +4903,25 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.100"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.50"
+version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
dependencies = [
"cfg-if",
+ "futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -4955,9 +4930,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.100"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4965,22 +4940,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.100"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
+ "bumpalo",
"proc-macro2",
"quote",
"syn",
- "wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.100"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
@@ -5073,12 +5048,12 @@ dependencies = [
[[package]]
name = "wayland-scanner"
-version = "0.31.7"
+version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
+checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [
"proc-macro2",
- "quick-xml 0.37.5",
+ "quick-xml",
"quote",
]
@@ -5096,9 +5071,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.77"
+version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5124,7 +5099,7 @@ dependencies = [
"jni",
"log",
"ndk-context",
- "objc2 0.6.3",
+ "objc2 0.6.4",
"objc2-foundation 0.3.2",
"url",
"web-sys",
@@ -5132,9 +5107,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
-version = "0.26.6"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
dependencies = [
"rustls-pki-types",
]
@@ -5147,16 +5122,17 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
[[package]]
name = "wgpu"
-version = "27.0.1"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
+checksum = "78f9f386699b1fb8b8a05bfe82169b24d151f05702d2905a0bf93bc454fcc825"
dependencies = [
"arrayvec",
"bitflags 2.9.4",
+ "bytemuck",
"cfg-if",
"cfg_aliases",
"document-features",
- "hashbrown 0.16.0",
+ "hashbrown 0.16.1",
"js-sys",
"log",
"naga",
@@ -5176,18 +5152,18 @@ dependencies = [
[[package]]
name = "wgpu-core"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "893764e276cdafec946c7f394f044e283bc8f1e445ab3fea8ad3b6dbc10c0322"
+checksum = "c7c34181b0acb8f98168f78f8e57ec66f57df5522b39143dbe5f2f45d7ca927c"
dependencies = [
"arrayvec",
- "bit-set",
- "bit-vec",
+ "bit-set 0.9.1",
+ "bit-vec 0.9.1",
"bitflags 2.9.4",
"bytemuck",
"cfg_aliases",
"document-features",
- "hashbrown 0.16.0",
+ "hashbrown 0.16.1",
"indexmap",
"log",
"naga",
@@ -5204,76 +5180,78 @@ dependencies = [
"wgpu-core-deps-wasm",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
+ "wgpu-naga-bridge",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-apple"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233"
+checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-emscripten"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5"
+checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-wasm"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7"
+checksum = "2f7b75e72f49035f000dd5262e4126242e92a090a4fd75931ecfe7e60784e6fa"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
+checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a753c3dc95e69be3aacfe9c871c5fa2cfa9e35748cdc87de7ba5fc1735b61604"
+checksum = "058b6047337cf323a4f092486443a9337f3d81325347e5d77deed7e563aeaedc"
dependencies = [
"android_system_properties",
"arrayvec",
"ash",
- "bit-set",
+ "bit-set 0.9.1",
"bitflags 2.9.4",
- "block",
+ "block2 0.6.2",
"bytemuck",
"cfg-if",
"cfg_aliases",
- "core-graphics-types 0.2.0",
"glow",
"glutin_wgl_sys",
- "gpu-alloc",
"gpu-allocator",
"gpu-descriptor",
- "hashbrown 0.16.0",
+ "hashbrown 0.16.1",
"js-sys",
"khronos-egl",
"libc",
"libloading",
"log",
- "metal",
"naga",
"ndk-sys",
- "objc",
+ "objc2 0.6.4",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.2",
+ "objc2-metal 0.3.2",
+ "objc2-quartz-core 0.3.2",
"once_cell",
"ordered-float",
"parking_lot",
@@ -5282,27 +5260,40 @@ dependencies = [
"profiling",
"range-alloc",
"raw-window-handle",
+ "raw-window-metal",
"renderdoc-sys",
"smallvec",
"thiserror 2.0.17",
"wasm-bindgen",
+ "wayland-sys",
"web-sys",
+ "wgpu-naga-bridge",
+ "wgpu-types",
+ "windows",
+ "windows-core",
+]
+
+[[package]]
+name = "wgpu-naga-bridge"
+version = "29.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b8e1e505095f24cb4a578f04b1421d456257dca7fac114d9d9dd3d978c34b8"
+dependencies = [
+ "naga",
"wgpu-types",
- "windows 0.58.0",
- "windows-core 0.58.0",
]
[[package]]
name = "wgpu-types"
-version = "27.0.0"
+version = "29.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d67453b02f7adc33c452d17da1c2cad813448221df1547bce9dd4b02d3558538"
+checksum = "d15ece45db77dd5451f11c0ce898334317ce8502d304a20454b531fdc0652fae"
dependencies = [
"bitflags 2.9.4",
"bytemuck",
"js-sys",
"log",
- "thiserror 2.0.17",
+ "raw-window-handle",
"web-sys",
]
@@ -5339,88 +5330,54 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
-version = "0.58.0"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
-dependencies = [
- "windows-core 0.58.0",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows"
-version = "0.61.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
- "windows-core 0.61.0",
+ "windows-core",
"windows-future",
- "windows-link 0.1.3",
"windows-numerics",
]
[[package]]
name = "windows-collections"
-version = "0.2.0"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
- "windows-core 0.61.0",
+ "windows-core",
]
[[package]]
name = "windows-core"
-version = "0.58.0"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
- "windows-implement 0.58.0",
- "windows-interface 0.58.0",
- "windows-result 0.2.0",
- "windows-strings 0.1.0",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.61.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
-dependencies = [
- "windows-implement 0.60.0",
- "windows-interface 0.59.1",
- "windows-link 0.1.3",
- "windows-result 0.3.2",
- "windows-strings 0.4.0",
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.1",
+ "windows-result",
+ "windows-strings",
]
[[package]]
name = "windows-future"
-version = "0.2.0"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
- "windows-core 0.61.0",
- "windows-link 0.1.3",
+ "windows-core",
+ "windows-link 0.2.1",
+ "windows-threading",
]
[[package]]
name = "windows-implement"
-version = "0.58.0"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
@@ -5429,20 +5386,9 @@ dependencies = [
[[package]]
name = "windows-interface"
-version = "0.58.0"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
@@ -5463,49 +5409,30 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
-version = "0.2.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
- "windows-core 0.61.0",
- "windows-link 0.1.3",
+ "windows-core",
+ "windows-link 0.2.1",
]
[[package]]
name = "windows-result"
-version = "0.2.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-result"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
-dependencies = [
- "windows-link 0.1.3",
+ "windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
-version = "0.1.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "windows-result 0.2.0",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
-dependencies = [
- "windows-link 0.1.3",
+ "windows-link 0.2.1",
]
[[package]]
@@ -5601,6 +5528,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@@ -5741,9 +5677,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winit"
-version = "0.30.12"
+version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
+checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
dependencies = [
"ahash",
"android-activity",
@@ -5765,7 +5701,7 @@ dependencies = [
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
- "objc2-ui-kit",
+ "objc2-ui-kit 0.2.2",
"orbclient",
"percent-encoding",
"pin-project",
@@ -5853,16 +5789,6 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
-[[package]]
-name = "xdg-home"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
-dependencies = [
- "libc",
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "xkbcommon-dl"
version = "0.4.2"
@@ -5933,13 +5859,12 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.5.0"
+version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236"
+checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
dependencies = [
"async-broadcast",
"async-executor",
- "async-fs",
"async-io",
"async-lock",
"async-process",
@@ -5956,12 +5881,11 @@ dependencies = [
"ordered-stream",
"serde",
"serde_repr",
- "static_assertions",
"tracing",
"uds_windows",
- "windows-sys 0.59.0",
+ "uuid",
+ "windows-sys 0.61.2",
"winnow",
- "xdg-home",
"zbus_macros",
"zbus_names",
"zvariant",
@@ -5969,9 +5893,9 @@ dependencies = [
[[package]]
name = "zbus-lockstep"
-version = "0.5.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a22426b1bc2aca91de97772506f0655fa373448e6010d79d5d5880915c388409"
+checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863"
dependencies = [
"zbus_xml",
"zvariant",
@@ -5979,9 +5903,9 @@ dependencies = [
[[package]]
name = "zbus-lockstep-macros"
-version = "0.5.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "100ffec29ed51859052f4563061abe35557acb56ba574510571f8398efc70a29"
+checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0"
dependencies = [
"proc-macro2",
"quote",
@@ -5993,9 +5917,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.5.0"
+version = "5.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0"
+checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -6008,57 +5932,34 @@ dependencies = [
[[package]]
name = "zbus_names"
-version = "4.2.0"
+version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
dependencies = [
"serde",
- "static_assertions",
"winnow",
"zvariant",
]
[[package]]
name = "zbus_xml"
-version = "5.0.2"
+version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29"
+checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac"
dependencies = [
- "quick-xml 0.36.2",
+ "quick-xml",
"serde",
- "static_assertions",
"zbus_names",
"zvariant",
]
-[[package]]
-name = "zerocopy"
-version = "0.7.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
-dependencies = [
- "byteorder",
- "zerocopy-derive 0.7.35",
-]
-
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
- "zerocopy-derive 0.8.27",
-]
-
-[[package]]
-name = "zerocopy-derive"
-version = "0.7.35"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "zerocopy-derive",
]
[[package]]
@@ -6095,9 +5996,9 @@ dependencies = [
[[package]]
name = "zeroize"
-version = "1.8.1"
+version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
@@ -6132,6 +6033,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
[[package]]
name = "zune-core"
version = "0.4.12"
@@ -6149,15 +6056,13 @@ dependencies = [
[[package]]
name = "zvariant"
-version = "5.4.0"
+version = "5.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac"
+checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4"
dependencies = [
"endi",
"enumflags2",
"serde",
- "static_assertions",
- "url",
"winnow",
"zvariant_derive",
"zvariant_utils",
@@ -6165,9 +6070,9 @@ dependencies = [
[[package]]
name = "zvariant_derive"
-version = "5.4.0"
+version = "5.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f"
+checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -6178,14 +6083,13 @@ dependencies = [
[[package]]
name = "zvariant_utils"
-version = "3.2.0"
+version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
dependencies = [
"proc-macro2",
"quote",
"serde",
- "static_assertions",
"syn",
"winnow",
]
diff --git a/Cargo.toml b/Cargo.toml
index 470644bb4..cf631eb25 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -68,9 +68,9 @@ egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features =
egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.33.3", path = "crates/eframe", default-features = false }
-accesskit = "0.21.1"
-accesskit_consumer = "0.30.1"
-accesskit_winit = "0.29.1"
+accesskit = "0.24.0"
+accesskit_consumer = "0.35.0"
+accesskit_winit = "0.32.0"
ahash = { version = "0.8.12", default-features = false, features = [
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
"std",
@@ -80,33 +80,34 @@ arboard = { version = "3.6.1", default-features = false }
backtrace = "0.3.76"
bitflags = "2.9.4"
bytemuck = "1.24.0"
-chrono = { version = "0.4.42", default-features = false }
cint = "0.3.1"
color-hex = "0.2.0"
criterion = { version = "0.7.0", default-features = false }
-dify = { version = "0.7.4", default-features = false }
+dify = { version = "0.8", default-features = false }
directories = "6.0.0"
document-features = "0.2.11"
-ehttp = { version = "0.6.0", default-features = false }
+ehttp = { version = "0.7.1", default-features = false }
enum-map = "2.7.3"
env_logger = { version = "0.11.8", default-features = false }
-glow = "0.16.0"
+font-types = { version = "0.11.0", default-features = false, features = ["std"] }
+glow = "0.17.0"
glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
home = "0.5.9"
image = { version = "0.25.6", default-features = false }
+jiff = { version = "0.2.23", default-features = false }
js-sys = "0.3.77"
-kittest = { version = "0.3.0" }
+kittest = { version = "0.4.0" }
log = { version = "0.4.28", features = ["std"] }
memoffset = "0.9.1"
mimalloc = "0.1.48"
mime_guess2 = { version = "2.3.1", default-features = false }
mint = "0.5.9"
nohash-hasher = "0.2.0"
-objc2 = "0.5.2"
-objc2-app-kit = { version = "0.2.2", default-features = false }
-objc2-foundation = { version = "0.2.2", default-features = false }
-objc2-ui-kit = { version = "0.2.2", default-features = false }
+objc2 = "0.6.4"
+objc2-app-kit = { version = "0.3.2", default-features = false }
+objc2-foundation = { version = "0.3.2", default-features = false }
+objc2-ui-kit = { version = "0.3.2", default-features = false }
open = "5.3.2"
parking_lot = "0.12.5"
percent-encoding = "2.3.2"
@@ -119,34 +120,33 @@ rand = "0.9.2"
raw-window-handle = "0.6.2"
rayon = "1.11.0"
resvg = { version = "0.45.1", default-features = false }
-rfd = "0.15.4"
-ron = "0.11.0"
+rfd = "0.17.2"
+ron = "0.12.0"
self_cell = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] }
similar-asserts = "1.7.0"
-skrifa = { version = "0.37.0", default-features = false, features = ["std", "autohint_shaping"] }
+skrifa = { version = "0.40.0", default-features = false, features = ["std", "autohint_shaping"] }
smallvec = "1.15.1"
smithay-clipboard = "0.7.2"
static_assertions = "1.1.0"
syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
-tokio = "1.47.1"
-toml = "0.8"
+tokio = "1.49"
+toml = {version = "1.0.0", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-segmentation = "1.12.0"
-vello_cpu = { version = "0.0.4", default-features = false, features = ["std"] }
-wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
-wasm-bindgen-futures = "0.4.0"
+vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
+wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
+wasm-bindgen-futures = "0.4.58"
wayland-cursor = { version = "0.31.11", default-features = false }
web-sys = "0.3.77"
web-time = "1.1.0" # Timekeeping for native and web
webbrowser = "1.0.5"
-wgpu = { version = "27.0.1", default-features = false, features = ["std"] }
+wgpu = { version = "29.0.0", default-features = false, features = ["std"] }
windows-sys = "0.61.2"
-winit = { version = "0.30.12", default-features = false }
-
+winit = { version = "0.30.13", default-features = false }
[workspace.lints.rust]
unsafe_code = "deny"
diff --git a/README.md b/README.md
index f4a094465..7eebd0423 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,14 @@
[](https://discord.gg/JFcEma9bJq)
+
-

+

-egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
-an SDK for visualizing streams of multimodal data.
-
-
----
+
👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈
+
egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations).
diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml
index f37aff109..43543113a 100644
--- a/crates/ecolor/Cargo.toml
+++ b/crates/ecolor/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml
index 86f63c50e..6219b90fd 100644
--- a/crates/eframe/Cargo.toml
+++ b/crates/eframe/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
[package.metadata.docs.rs]
all-features = true
@@ -169,7 +169,10 @@ objc2-foundation = { workspace = true, default-features = false, features = [
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
+ "NSBitmapImageRep",
+ "NSGraphics",
"NSImage",
+ "NSImageRep",
"NSMenu",
"NSMenuItem",
"NSResponder",
diff --git a/crates/eframe/data/icon.png b/crates/eframe/data/icon.png
index cf1e6c3eb..4ce7cc588 100644
Binary files a/crates/eframe/data/icon.png and b/crates/eframe/data/icon.png differ
diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs
index c37dc1cf6..b9a178a1d 100644
--- a/crates/eframe/src/epi.rs
+++ b/crates/eframe/src/epi.rs
@@ -72,7 +72,8 @@ pub struct CreationContext<'s> {
/// The `get_proc_address` wrapper of underlying GL context
#[cfg(feature = "glow")]
- pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>,
+ pub get_proc_address:
+ Option *const std::ffi::c_void + Send + Sync>>,
/// The underlying WGPU render state.
///
diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs
index 3ac61d8e6..85be6754b 100644
--- a/crates/eframe/src/native/app_icon.rs
+++ b/crates/eframe/src/native/app_icon.rs
@@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
- use objc2::ClassType as _;
+ use objc2::AnyThread as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::NSString;
diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs
index 96a52db88..5b22eb08c 100644
--- a/crates/eframe/src/native/epi_integration.rs
+++ b/crates/eframe/src/native/epi_integration.rs
@@ -265,6 +265,7 @@ impl EpiIntegration {
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
+ is_visible: bool,
) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
@@ -275,23 +276,27 @@ impl EpiIntegration {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
- profiling::scope!("viewport_callback");
- viewport_ui_cb(ui);
+ if is_visible {
+ profiling::scope!("viewport_callback");
+ viewport_ui_cb(ui);
+ }
} else {
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}
- {
- profiling::scope!("App::update");
- #[expect(deprecated)]
- app.update(ui.ctx(), &mut self.frame);
- }
+ if is_visible {
+ {
+ profiling::scope!("App::update");
+ #[expect(deprecated)]
+ app.update(ui.ctx(), &mut self.frame);
+ }
- {
- profiling::scope!("App::ui");
- app.ui(ui, &mut self.frame);
+ {
+ profiling::scope!("App::ui");
+ app.ui(ui, &mut self.frame);
+ }
}
}
});
diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs
index d72bdf26f..2f4916b1f 100644
--- a/crates/eframe/src/native/glow_integration.rs
+++ b/crates/eframe/src/native/glow_integration.rs
@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
- native::epi_integration::EpiIntegration,
+ native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use super::{
@@ -294,14 +294,15 @@ impl<'app> GlowWinitApp<'app> {
// Use latest raw_window_handle for eframe compatibility
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
- let get_proc_address = |addr: &_| glutin.get_proc_address(addr);
+ let gl_config = glutin.gl_config.clone();
+ let get_proc_address = move |addr: &_| gl_config.display().get_proc_address(addr);
let window = glutin.window(ViewportId::ROOT);
let cc = CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(),
gl: Some(gl),
- get_proc_address: Some(&get_proc_address),
+ get_proc_address: Some(Arc::new(get_proc_address)),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
@@ -466,12 +467,21 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
+ && let Some(window) = viewport.window.as_ref()
{
- if let Some(egui_winit) = viewport.egui_winit.as_mut() {
- egui_winit.on_mouse_motion(delta);
+ if !window.has_focus()
+ && !viewport
+ .egui_winit
+ .as_ref()
+ .map(|state| state.is_any_pointer_button_down())
+ .unwrap_or(false)
+ {
+ return Ok(EventResult::Wait);
}
- if let Some(window) = viewport.window.as_ref() {
+ if let Some(egui_winit) = viewport.egui_winit.as_mut()
+ && egui_winit.on_mouse_motion(delta)
+ {
return Ok(EventResult::RepaintNext(window.id()));
}
}
@@ -554,7 +564,7 @@ impl GlowWinitRunning<'_> {
}
}
- let (raw_input, viewport_ui_cb) = {
+ let (raw_input, viewport_ui_cb, is_visible) = {
let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
@@ -565,6 +575,8 @@ impl GlowWinitRunning<'_> {
};
egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false);
+ let is_visible = viewport.info.visible().unwrap_or(true);
+
let Some(egui_winit) = viewport.egui_winit.as_mut() else {
return Ok(EventResult::Wait);
};
@@ -580,7 +592,7 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.collect();
- (raw_input, viewport_ui_cb)
+ (raw_input, viewport_ui_cb, is_visible)
};
// HACK: In order to get the right clear_color, the system theme needs to be set, which
@@ -596,7 +608,7 @@ impl GlowWinitRunning<'_> {
let has_many_viewports = self.glutin.borrow().viewports.len() > 1;
let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports.
- if clear_before_update {
+ if is_visible && clear_before_update {
// clear before we call update, so users can paint between clear-color and egui windows:
let mut glutin = self.glutin.borrow_mut();
@@ -631,9 +643,12 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
- let full_output =
- self.integration
- .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
+ let full_output = self.integration.update(
+ self.app.as_mut(),
+ viewport_ui_cb.as_deref(),
+ raw_input,
+ is_visible,
+ );
// ------------------------------------------------------------
@@ -676,85 +691,87 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output(&window, platform_output);
- let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
+ if is_visible {
+ let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
- {
- // We may need to switch contexts again, because of immediate viewports:
- frame_timer.pause();
- change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
- frame_timer.resume();
- }
+ {
+ // We may need to switch contexts again, because of immediate viewports:
+ frame_timer.pause();
+ change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
+ frame_timer.resume();
+ }
- let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
+ let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
- if !clear_before_update {
- painter.clear(screen_size_in_pixels, clear_color);
- }
+ if !clear_before_update {
+ painter.clear(screen_size_in_pixels, clear_color);
+ }
- painter.paint_and_update_textures(
- screen_size_in_pixels,
- pixels_per_point,
- &clipped_primitives,
- &textures_delta,
- );
+ painter.paint_and_update_textures(
+ screen_size_in_pixels,
+ pixels_per_point,
+ &clipped_primitives,
+ &textures_delta,
+ );
- {
- for action in viewport.actions_requested.drain(..) {
- match action {
- ActionRequested::Screenshot(user_data) => {
- let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
- egui_winit
- .egui_input_mut()
- .events
- .push(egui::Event::Screenshot {
- viewport_id,
- user_data,
- image: screenshot.into(),
- });
- }
- ActionRequested::Cut => {
- egui_winit.egui_input_mut().events.push(egui::Event::Cut);
- }
- ActionRequested::Copy => {
- egui_winit.egui_input_mut().events.push(egui::Event::Copy);
- }
- ActionRequested::Paste => {
- if let Some(contents) = egui_winit.clipboard_text() {
- let contents = contents.replace("\r\n", "\n");
- if !contents.is_empty() {
- egui_winit
- .egui_input_mut()
- .events
- .push(egui::Event::Paste(contents));
+ {
+ for action in viewport.actions_requested.drain(..) {
+ match action {
+ ActionRequested::Screenshot(user_data) => {
+ let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
+ egui_winit
+ .egui_input_mut()
+ .events
+ .push(egui::Event::Screenshot {
+ viewport_id,
+ user_data,
+ image: screenshot.into(),
+ });
+ }
+ ActionRequested::Cut => {
+ egui_winit.egui_input_mut().events.push(egui::Event::Cut);
+ }
+ ActionRequested::Copy => {
+ egui_winit.egui_input_mut().events.push(egui::Event::Copy);
+ }
+ ActionRequested::Paste => {
+ if let Some(contents) = egui_winit.clipboard_text() {
+ let contents = contents.replace("\r\n", "\n");
+ if !contents.is_empty() {
+ egui_winit
+ .egui_input_mut()
+ .events
+ .push(egui::Event::Paste(contents));
+ }
}
}
}
}
+
+ integration.post_rendering(&window);
}
- integration.post_rendering(&window);
- }
+ {
+ // vsync - don't count as frame-time:
+ frame_timer.pause();
+ profiling::scope!("swap_buffers");
+ let context = current_gl_context.as_ref().ok_or_else(|| {
+ egui_glow::PainterError::from(
+ "failed to get current context to swap buffers".to_owned(),
+ )
+ })?;
- {
- // vsync - don't count as frame-time:
- frame_timer.pause();
- profiling::scope!("swap_buffers");
- let context = current_gl_context.as_ref().ok_or_else(|| {
- egui_glow::PainterError::from(
- "failed to get current context to swap buffers".to_owned(),
- )
- })?;
+ gl_surface.swap_buffers(context)?;
+ frame_timer.resume();
+ }
- gl_surface.swap_buffers(context)?;
- frame_timer.resume();
- }
-
- // give it time to settle:
- #[cfg(feature = "__screenshot")]
- if integration.egui_ctx.cumulative_pass_nr() == 2
- && let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
- {
- save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
+ // give it time to settle:
+ #[cfg(feature = "__screenshot")]
+ if integration.egui_ctx.cumulative_pass_nr() == 2
+ && let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
+ {
+ save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
+ }
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
@@ -763,9 +780,11 @@ impl GlowWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), Some(&window));
- if window.is_minimized() == Some(true) {
+ if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
+ // On Windows, an invisible window also uses up all CPU:
+ // https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -831,6 +850,14 @@ impl GlowWinitRunning<'_> {
}
}
+ winit::event::WindowEvent::Occluded(is_occluded) => {
+ if let Some(viewport_id) = viewport_id
+ && let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
+ {
+ viewport.info.occluded = Some(*is_occluded);
+ }
+ }
+
winit::event::WindowEvent::CloseRequested => {
if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() {
log::debug!(
diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs
index 0597d318c..73b58ae61 100644
--- a/crates/eframe/src/native/run.rs
+++ b/crates/eframe/src/native/run.rs
@@ -1,4 +1,4 @@
-use std::time::Instant;
+use std::time::{Duration, Instant};
use winit::{
application::ApplicationHandler,
@@ -11,9 +11,20 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
Result, epi,
- native::{event_loop_context, winit_integration::EventResult},
+ native::{
+ event_loop_context,
+ winit_integration::{EventResult, is_invisible_or_minimized},
+ },
};
+/// Minimum interval between repaints for invisible windows.
+///
+/// On Windows, invisible windows don't receive `RedrawRequested` events,
+/// so we throttle their repaints to avoid busy-looping while still
+/// processing viewport commands like `Visible(true)`.
+/// See .
+const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
+
// ----------------------------------------------------------------------------
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> {
#[cfg(target_os = "android")]
@@ -177,23 +188,54 @@ impl WinitAppWrapper {
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
let now = Instant::now();
+ let mut invisible_window_ids = Vec::new();
+
self.windows_next_repaint_times
.retain(|window_id, repaint_time| {
if now < *repaint_time {
return true; // not yet ready
}
- event_loop.set_control_flow(ControlFlow::Poll);
-
if let Some(window) = self.winit_app.window(*window_id) {
- log::trace!("request_redraw for {window_id:?}");
- window.request_redraw();
+ // On Windows, invisible windows don't receive RedrawRequested
+ // events, so pending viewport commands (e.g. Visible(true)) would
+ // never be processed. We collect these windows to paint them
+ // directly below.
+ // See: https://github.com/emilk/egui/issues/5229
+ if is_invisible_or_minimized(&window) {
+ invisible_window_ids.push(*window_id);
+ } else {
+ log::trace!("request_redraw for {window_id:?}");
+ event_loop.set_control_flow(ControlFlow::Poll);
+ window.request_redraw();
+ }
} else {
log::trace!("No window found for {window_id:?}");
}
false
});
+ // Paint invisible windows directly, since they won't receive
+ // RedrawRequested events on Windows. This ensures that viewport
+ // commands like Visible(true) are still processed.
+ for window_id in &invisible_window_ids {
+ let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id);
+ self.handle_event_result(event_loop, event_result);
+ }
+
+ // Throttle any already-scheduled repaints for invisible windows
+ // to avoid busy-looping. If no repaint was requested by the app,
+ // the window will simply sleep.
+ // See: https://github.com/emilk/egui/issues/7776
+ if !invisible_window_ids.is_empty() {
+ let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL;
+ for window_id in &invisible_window_ids {
+ self.windows_next_repaint_times
+ .entry(*window_id)
+ .and_modify(|t| *t = (*t).min(next_paint));
+ }
+ }
+
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
@@ -270,6 +312,16 @@ impl ApplicationHandler for WinitAppWrapper {
if let Some(window_id) =
self.winit_app.window_id_from_viewport_id(viewport_id)
{
+ // Throttle repaints for invisible windows to prevent
+ // high CPU usage on Windows.
+ // See: https://github.com/emilk/egui/issues/7776
+ let when = if let Some(window) = self.winit_app.window(window_id)
+ && is_invisible_or_minimized(&window)
+ {
+ when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL)
+ } else {
+ when
+ };
Ok(EventResult::RepaintAt(window_id, when))
} else {
Ok(EventResult::Wait)
diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs
index b4eef08b5..4ca491baf 100644
--- a/crates/eframe/src/native/wgpu_integration.rs
+++ b/crates/eframe/src/native/wgpu_integration.rs
@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
- native::{epi_integration::EpiIntegration, winit_integration::EventResult},
+ native::{
+ epi_integration::EpiIntegration,
+ winit_integration::{EventResult, is_invisible_or_minimized},
+ },
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
@@ -184,9 +187,17 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
+ // Inject the display handle into the wgpu setup so that wgpu can create
+ // surfaces on platforms that require it (e.g. GLES on Wayland).
+ let mut wgpu_options = self.native_options.wgpu_options.clone();
+ if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
+ && create_new.display_handle.is_none()
+ {
+ create_new.display_handle = Some(Box::new(event_loop.owned_display_handle()));
+ }
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
- self.native_options.wgpu_options.clone(),
+ wgpu_options,
self.native_options.viewport.transparent.unwrap_or(false),
egui_wgpu::RendererOptions {
msaa_samples: self.native_options.multisampling as _,
@@ -474,12 +485,21 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
+ && let Some(window) = viewport.window.as_ref()
{
- if let Some(egui_winit) = viewport.egui_winit.as_mut() {
- egui_winit.on_mouse_motion(delta);
+ if !window.has_focus()
+ && !viewport
+ .egui_winit
+ .as_ref()
+ .map(|state| state.is_any_pointer_button_down())
+ .unwrap_or(false)
+ {
+ return Ok(EventResult::Wait);
}
- if let Some(window) = viewport.window.as_ref() {
+ if let Some(egui_winit) = viewport.egui_winit.as_mut()
+ && egui_winit.on_mouse_motion(delta)
+ {
return Ok(EventResult::RepaintNext(window.id()));
}
}
@@ -584,7 +604,7 @@ impl WgpuWinitRunning<'_> {
let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start();
- let (viewport_ui_cb, raw_input) = {
+ let (viewport_ui_cb, raw_input, is_visible) = {
profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut();
@@ -628,6 +648,8 @@ impl WgpuWinitRunning<'_> {
};
egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false);
+ let is_visible = viewport.info.visible().unwrap_or(true);
+
{
profiling::scope!("set_window");
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?;
@@ -648,14 +670,19 @@ impl WgpuWinitRunning<'_> {
painter.handle_screenshots(&mut raw_input.events);
- (viewport_ui_cb, raw_input)
+ (viewport_ui_cb, raw_input, is_visible)
};
// ------------------------------------------------------------
// Runs the update, which could call immediate viewports,
// so make sure we hold no locks here!
- let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
+ let full_output = integration.update(
+ app.as_mut(),
+ viewport_ui_cb.as_deref(),
+ raw_input,
+ is_visible,
+ );
// ------------------------------------------------------------
@@ -696,52 +723,58 @@ impl WgpuWinitRunning<'_> {
egui_winit.handle_platform_output(window, platform_output);
- let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
+ let vsync_secs = if is_visible {
+ let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
- let mut screenshot_commands = vec![];
- viewport.actions_requested.retain(|cmd| {
- if let ActionRequested::Screenshot(info) = cmd {
- screenshot_commands.push(info.clone());
- false
- } else {
- true
- }
- });
- let vsync_secs = painter.paint_and_update_textures(
- viewport_id,
- pixels_per_point,
- app.clear_color(&egui_ctx.global_style().visuals),
- &clipped_primitives,
- &textures_delta,
- screenshot_commands,
- );
+ let mut screenshot_commands = vec![];
+ viewport.actions_requested.retain(|cmd| {
+ if let ActionRequested::Screenshot(info) = cmd {
+ screenshot_commands.push(info.clone());
+ false
+ } else {
+ true
+ }
+ });
+ let vsync_secs = painter.paint_and_update_textures(
+ viewport_id,
+ pixels_per_point,
+ app.clear_color(&egui_ctx.global_style().visuals),
+ &clipped_primitives,
+ &textures_delta,
+ screenshot_commands,
+ );
- for action in viewport.actions_requested.drain(..) {
- match action {
- ActionRequested::Screenshot { .. } => {
- // already handled above
- }
- ActionRequested::Cut => {
- egui_winit.egui_input_mut().events.push(egui::Event::Cut);
- }
- ActionRequested::Copy => {
- egui_winit.egui_input_mut().events.push(egui::Event::Copy);
- }
- ActionRequested::Paste => {
- if let Some(contents) = egui_winit.clipboard_text() {
- let contents = contents.replace("\r\n", "\n");
- if !contents.is_empty() {
- egui_winit
- .egui_input_mut()
- .events
- .push(egui::Event::Paste(contents));
+ for action in viewport.actions_requested.drain(..) {
+ match action {
+ ActionRequested::Screenshot { .. } => {
+ // already handled above
+ }
+ ActionRequested::Cut => {
+ egui_winit.egui_input_mut().events.push(egui::Event::Cut);
+ }
+ ActionRequested::Copy => {
+ egui_winit.egui_input_mut().events.push(egui::Event::Copy);
+ }
+ ActionRequested::Paste => {
+ if let Some(contents) = egui_winit.clipboard_text() {
+ let contents = contents.replace("\r\n", "\n");
+ if !contents.is_empty() {
+ egui_winit
+ .egui_input_mut()
+ .events
+ .push(egui::Event::Paste(contents));
+ }
}
}
}
}
- }
- integration.post_rendering(window);
+ integration.post_rendering(window);
+
+ vsync_secs
+ } else {
+ 0.0
+ };
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
@@ -768,10 +801,12 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
- && window.is_minimized() == Some(true)
+ && is_invisible_or_minimized(window)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
+ // On Windows, an invisible window also uses up all CPU:
+ // https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -863,6 +898,14 @@ impl WgpuWinitRunning<'_> {
}
}
+ winit::event::WindowEvent::Occluded(is_occluded) => {
+ if let Some(viewport_id) = viewport_id
+ && let Some(viewport) = shared.viewports.get_mut(&viewport_id)
+ {
+ viewport.info.occluded = Some(*is_occluded);
+ }
+ }
+
winit::event::WindowEvent::CloseRequested => {
if viewport_id == Some(ViewportId::ROOT) && integration.should_close() {
log::debug!(
diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs
index 012c22f8e..b4ec62c09 100644
--- a/crates/eframe/src/native/winit_integration.rs
+++ b/crates/eframe/src/native/winit_integration.rs
@@ -9,6 +9,14 @@ use egui::ViewportId;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
+/// Returns `true` if the window is invisible or minimized.
+///
+/// These windows don't receive `RedrawRequested` events on Windows,
+/// so they need special handling to keep processing viewport commands.
+pub fn is_invisible_or_minimized(window: &Window) -> bool {
+ window.is_visible() == Some(false) || window.is_minimized() == Some(true)
+}
+
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();
diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs
index 11654135d..b90b8a5e1 100644
--- a/crates/eframe/src/web/app_runner.rs
+++ b/crates/eframe/src/web/app_runner.rs
@@ -274,13 +274,21 @@ impl AppRunner {
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
+ let is_visible = raw_input
+ .viewports
+ .get(&egui::ViewportId::ROOT)
+ .and_then(|v| v.visible())
+ .unwrap_or(true);
+
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
- #[expect(deprecated)]
- self.app.update(ui.ctx(), &mut self.frame);
+ if is_visible {
+ #[expect(deprecated)]
+ self.app.update(ui.ctx(), &mut self.frame);
- self.app.ui(ui, &mut self.frame);
+ self.app.ui(ui, &mut self.frame);
+ }
});
let egui::FullOutput {
platform_output,
@@ -311,8 +319,10 @@ impl AppRunner {
}
self.handle_platform_output(platform_output);
- self.textures_delta.append(textures_delta);
- self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
+ if is_visible {
+ self.textures_delta.append(textures_delta);
+ self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
+ }
}
/// Paint the results of the last call to [`Self::logic`].
diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs
index 4814fa99b..e2724fc49 100644
--- a/crates/eframe/src/web/backend.rs
+++ b/crates/eframe/src/web/backend.rs
@@ -31,11 +31,18 @@ impl WebInput {
time: Some(super::now_sec()),
..self.raw.take()
};
- raw_input
+ let viewport = raw_input
.viewports
.entry(egui::ViewportId::ROOT)
- .or_default()
- .native_pixels_per_point = Some(super::native_pixels_per_point());
+ .or_default();
+ viewport.native_pixels_per_point = Some(super::native_pixels_per_point());
+
+ // A hidden browser tab is effectively occluded.
+ let hidden = web_sys::window()
+ .and_then(|w| w.document())
+ .is_some_and(|doc| doc.hidden());
+ viewport.occluded = Some(hidden);
+
raw_input
}
diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs
index 1e54d7a84..87771f722 100644
--- a/crates/eframe/src/web/mod.rs
+++ b/crates/eframe/src/web/mod.rs
@@ -38,6 +38,7 @@ mod web_painter_wgpu;
pub use backend::*;
use egui::Theme;
+use js_sys::Object;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -370,5 +371,5 @@ pub fn percent_decode(s: &str) -> String {
/// Are we running inside the Safari browser?
pub fn is_safari_browser() -> bool {
- web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari")))
+ web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari")))
}
diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs
index 264ce6adc..63702592d 100644
--- a/crates/eframe/src/web/web_painter_wgpu.rs
+++ b/crates/eframe/src/web/web_painter_wgpu.rs
@@ -15,13 +15,32 @@ pub(crate) struct WebPainterWgpu {
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option,
- on_surface_error: Arc SurfaceErrorAction>,
+ on_surface_status: Arc SurfaceErrorAction>,
depth_stencil_format: Option,
depth_texture_view: Option,
screen_capture_state: Option,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
ctx: egui::Context,
+ needs_reconfigure: bool,
+}
+
+/// Owned web display handle that is `Send + Sync`.
+///
+/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
+/// contains platform variants with raw pointers. On web the handle is always empty,
+/// so this wrapper is safe.
+#[cfg(target_arch = "wasm32")]
+#[derive(Clone, Debug)]
+struct WebDisplay;
+
+#[cfg(target_arch = "wasm32")]
+impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay {
+ fn display_handle(
+ &self,
+ ) -> Result, egui_wgpu::wgpu::rwh::HandleError> {
+ Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web())
+ }
}
impl WebPainterWgpu {
@@ -63,7 +82,17 @@ impl WebPainterWgpu {
) -> Result {
log::debug!("Creating wgpu painter");
- let instance = options.wgpu_options.wgpu_setup.new_instance().await;
+ // Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL.
+ let mut wgpu_options = options.wgpu_options.clone();
+ if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
+ && create_new.display_handle.is_none()
+ {
+ // Force WebGL, useful for quick & dirty testing:
+ //create_new.instance_descriptor.backends = wgpu::Backends::GL;
+ create_new.display_handle = Some(Box::new(WebDisplay));
+ }
+
+ let instance = wgpu_options.wgpu_setup.new_instance().await;
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
@@ -71,7 +100,7 @@ impl WebPainterWgpu {
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let render_state = RenderState::create(
- &options.wgpu_options,
+ &wgpu_options,
&instance,
Some(&surface),
egui_wgpu::RendererOptions {
@@ -89,7 +118,7 @@ impl WebPainterWgpu {
let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
- present_mode: options.wgpu_options.present_mode,
+ present_mode: wgpu_options.present_mode,
view_formats: vec![render_state.target_format],
..default_configuration
};
@@ -105,11 +134,12 @@ impl WebPainterWgpu {
surface_configuration,
depth_stencil_format,
depth_texture_view: None,
- on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
+ on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
screen_capture_state: None,
capture_tx,
capture_rx,
ctx,
+ needs_reconfigure: false,
})
}
}
@@ -195,18 +225,28 @@ impl WebPainter for WebPainterWgpu {
);
}
+ if self.needs_reconfigure {
+ self.surface
+ .configure(&render_state.device, &self.surface_configuration);
+ self.needs_reconfigure = false;
+ }
+
let output_frame = match self.surface.get_current_texture() {
- Ok(frame) => frame,
- Err(err) => match (*self.on_surface_error)(err) {
- SurfaceErrorAction::RecreateSurface => {
- self.surface
- .configure(&render_state.device, &self.surface_configuration);
- return Ok(());
+ wgpu::CurrentSurfaceTexture::Success(frame) => frame,
+ wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
+ self.needs_reconfigure = true;
+ frame
+ }
+ other => {
+ match (*self.on_surface_status)(&other) {
+ SurfaceErrorAction::RecreateSurface => {
+ self.surface
+ .configure(&render_state.device, &self.surface_configuration);
+ }
+ SurfaceErrorAction::SkipFrame => {}
}
- SurfaceErrorAction::SkipFrame => {
- return Ok(());
- }
- },
+ return Ok(());
+ }
};
{
@@ -268,6 +308,7 @@ impl WebPainter for WebPainterWgpu {
label: Some("egui_render"),
occlusion_query_set: None,
timestamp_writes: None,
+ multiview_mask: None,
});
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
@@ -280,15 +321,13 @@ impl WebPainter for WebPainterWgpu {
);
}
- let mut capture_buffer = None;
-
- if capture && let Some(capture_state) = &mut self.screen_capture_state {
- capture_buffer = Some(capture_state.copy_textures(
- &render_state.device,
- &output_frame,
- &mut encoder,
- ));
- }
+ let capture_buffer = if capture
+ && let Some(capture_state) = &mut self.screen_capture_state
+ {
+ Some(capture_state.copy_textures(&render_state.device, &output_frame, &mut encoder))
+ } else {
+ None
+ };
Some((output_frame, capture_buffer))
};
diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml
index c514e0a49..86cd3192b 100644
--- a/crates/egui-wgpu/Cargo.toml
+++ b/crates/egui-wgpu/Cargo.toml
@@ -15,7 +15,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
[lints]
workspace = true
diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs
index 58407fdd6..7eb1bd3ca 100644
--- a/crates/egui-wgpu/src/capture.rs
+++ b/crates/egui-wgpu/src/capture.rs
@@ -47,7 +47,7 @@ impl CaptureState {
},
depth_stencil: None,
multisample: MultisampleState::default(),
- multiview: None,
+ multiview_mask: None,
cache: None,
});
@@ -165,6 +165,7 @@ impl CaptureState {
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
+ multiview_mask: None,
});
pass.set_pipeline(&self.pipeline);
diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs
index 880ab8f4a..eb04173b5 100644
--- a/crates/egui-wgpu/src/lib.rs
+++ b/crates/egui-wgpu/src/lib.rs
@@ -24,7 +24,10 @@ mod renderer;
mod setup;
pub use renderer::*;
-pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
+pub use setup::{
+ EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
+ WgpuSetupExisting,
+};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
@@ -185,12 +188,13 @@ impl RenderState {
wgpu::Backends::all()
};
- instance.enumerate_adapters(backends)
+ instance.enumerate_adapters(backends).await
};
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
+ display_handle: _,
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
@@ -272,7 +276,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
}
}
-/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
+/// Specifies which action should be taken as consequence of a surface error.
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
@@ -299,8 +303,15 @@ pub struct WgpuConfiguration {
/// How to create the wgpu adapter & device
pub wgpu_setup: WgpuSetup,
- /// Callback for surface errors.
- pub on_surface_error: Arc SurfaceErrorAction + Send + Sync>,
+ /// Callback for surface status changes.
+ ///
+ /// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
+ /// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
+ /// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
+ /// defers surface reconfiguration to the next frame — the callback is not invoked
+ /// in that case either.
+ pub on_surface_status:
+ Arc SurfaceErrorAction + Send + Sync>,
}
#[test]
@@ -315,7 +326,7 @@ impl std::fmt::Debug for WgpuConfiguration {
present_mode,
desired_maximum_frame_latency,
wgpu_setup,
- on_surface_error: _,
+ on_surface_status: _,
} = self;
f.debug_struct("WgpuConfiguration")
.field("present_mode", &present_mode)
@@ -333,15 +344,25 @@ impl Default for WgpuConfiguration {
Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
- wgpu_setup: Default::default(),
- on_surface_error: Arc::new(|err| {
- if err == wgpu::SurfaceError::Outdated {
- // This error occurs when the app is minimized on Windows.
- // Silently return here to prevent spamming the console with:
- // "The underlying surface has changed, and therefore the swap chain must be updated"
- } else {
- log::warn!("Dropped frame with error: {err}");
+ // No display handle available at this point — callers should replace this with
+ // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
+ wgpu_setup: WgpuSetup::without_display_handle(),
+ on_surface_status: Arc::new(|status| {
+ match status {
+ wgpu::CurrentSurfaceTexture::Outdated => {
+ // This error occurs when the app is minimized on Windows.
+ // Silently return here to prevent spamming the console with:
+ // "The underlying surface has changed, and therefore the swap chain must be updated"
+ }
+ wgpu::CurrentSurfaceTexture::Occluded => {
+ // This error occurs when the application is occluded (e.g. minimized or behind another window).
+ log::debug!("Dropped frame with error: {status:?}");
+ }
+ _ => {
+ log::warn!("Dropped frame with error: {status:?}");
+ }
}
+
SurfaceErrorAction::SkipFrame
}),
}
@@ -395,6 +416,10 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
driver,
driver_info,
backend,
+ device_pci_bus_id,
+ subgroup_min_size,
+ subgroup_max_size,
+ transient_saves_memory,
} = &info;
// Example values:
@@ -426,6 +451,13 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
if *device != 0 {
summary += &format!(", device: 0x{device:02X}");
}
+ if !device_pci_bus_id.is_empty() {
+ summary += &format!(", pci_bus_id: {device_pci_bus_id:?}");
+ }
+ if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
+ summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}");
+ }
+ summary += &format!(", transient_saves_memory: {transient_saves_memory}");
summary
}
diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs
index d3d21f19c..e55f7581a 100644
--- a/crates/egui-wgpu/src/renderer.rs
+++ b/crates/egui-wgpu/src/renderer.rs
@@ -1,5 +1,3 @@
-#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
-
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
@@ -352,16 +350,19 @@ impl Renderer {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
- bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
- push_constant_ranges: &[],
+ bind_group_layouts: &[
+ Some(&uniform_bind_group_layout),
+ Some(&texture_bind_group_layout),
+ ],
+ immediate_size: 0,
});
let depth_stencil = options
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
- depth_write_enabled: false,
- depth_compare: wgpu::CompareFunction::Always,
+ depth_write_enabled: Some(false),
+ depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
@@ -426,7 +427,7 @@ impl Renderer {
})],
compilation_options: wgpu::PipelineCompilationOptions::default()
}),
- multiview: None,
+ multiview_mask: None,
cache: None,
}
)
@@ -469,6 +470,9 @@ impl Renderer {
/// The render pass internally keeps all referenced resources alive as long as necessary.
/// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
/// instead of a compile time error.
+ ///
+ /// # Panic
+ /// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic!
pub fn render(
&self,
render_pass: &mut wgpu::RenderPass<'static>,
@@ -513,8 +517,12 @@ impl Renderer {
// Skip rendering zero-sized clip areas.
if let Primitive::Mesh(_) = primitive {
// If this is a mesh, we need to advance the index and vertex buffer iterators:
- index_buffer_slices.next().unwrap();
- vertex_buffer_slices.next().unwrap();
+ index_buffer_slices
+ .next()
+ .expect("You must call .update_buffers() before .render()");
+ vertex_buffer_slices
+ .next()
+ .expect("You must call .update_buffers() before .render()");
}
continue;
}
@@ -524,8 +532,12 @@ impl Renderer {
match primitive {
Primitive::Mesh(mesh) => {
- let index_buffer_slice = index_buffer_slices.next().unwrap();
- let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
+ let index_buffer_slice = index_buffer_slices
+ .next()
+ .expect("You must call .update_buffers() before .render()");
+ let vertex_buffer_slice = vertex_buffer_slices
+ .next()
+ .expect("You must call .update_buffers() before .render()");
if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
render_pass.set_bind_group(1, bind_group, &[]);
@@ -951,6 +963,7 @@ impl Renderer {
let index_buffer_staging = queue.write_buffer_with(
&self.index_buffer.buffer,
0,
+ #[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_index_buffer_size).unwrap(),
);
@@ -968,7 +981,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.indices.len() * std::mem::size_of::();
let slice = index_offset..(size + index_offset);
- index_buffer_staging[slice.clone()]
+ index_buffer_staging
+ .slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
self.index_buffer.slices.push(slice);
index_offset += size;
@@ -994,6 +1008,7 @@ impl Renderer {
let vertex_buffer_staging = queue.write_buffer_with(
&self.vertex_buffer.buffer,
0,
+ #[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_vertex_buffer_size).unwrap(),
);
@@ -1011,7 +1026,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.vertices.len() * std::mem::size_of::();
let slice = vertex_offset..(size + vertex_offset);
- vertex_buffer_staging[slice.clone()]
+ vertex_buffer_staging
+ .slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
self.vertex_buffer.slices.push(slice);
vertex_offset += size;
diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs
index 0c3cb8c39..c5b3f0421 100644
--- a/crates/egui-wgpu/src/setup.rs
+++ b/crates/egui-wgpu/src/setup.rs
@@ -1,5 +1,43 @@
use std::sync::Arc;
+/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
+///
+/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
+/// `Box`. This trait wraps it so it can be cloned
+/// alongside the rest of the egui wgpu configuration.
+///
+/// Automatically implemented for all types that satisfy the bounds
+/// (including [`winit::event_loop::OwnedDisplayHandle`]).
+pub trait EguiDisplayHandle:
+ wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
+{
+ /// Clone into a `Box` for [`wgpu::InstanceDescriptor::display`].
+ fn clone_for_wgpu(&self) -> Box;
+
+ /// Clone into a new `Box`.
+ fn clone_display_handle(&self) -> Box;
+}
+
+impl Clone for Box {
+ fn clone(&self) -> Self {
+ // We need to deref here, otherwise this causes infinite recursion stack overflow.
+ (**self).clone_display_handle()
+ }
+}
+
+impl EguiDisplayHandle for T
+where
+ T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
+{
+ fn clone_for_wgpu(&self) -> Box {
+ Box::new(self.clone())
+ }
+
+ fn clone_display_handle(&self) -> Box {
+ Box::new(self.clone())
+ }
+}
+
#[derive(Clone)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
@@ -22,9 +60,19 @@ pub enum WgpuSetup {
Existing(WgpuSetupExisting),
}
-impl Default for WgpuSetup {
- fn default() -> Self {
- Self::CreateNew(WgpuSetupCreateNew::default())
+impl WgpuSetup {
+ /// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
+ ///
+ /// See [`WgpuSetupCreateNew::from_display_handle`] for details.
+ pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
+ Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
+ }
+
+ /// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
+ ///
+ /// See [`WgpuSetupCreateNew::without_display_handle`] for details.
+ pub fn without_display_handle() -> Self {
+ Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
}
}
@@ -65,8 +113,18 @@ impl WgpuSetup {
}
log::debug!("Creating wgpu instance with backends {backends:?}");
- wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
- .await
+ let desc = &create_new.instance_descriptor;
+ let descriptor = wgpu::InstanceDescriptor {
+ backends: desc.backends,
+ flags: desc.flags,
+ backend_options: desc.backend_options.clone(),
+ memory_budget_thresholds: desc.memory_budget_thresholds,
+ display: create_new
+ .display_handle
+ .as_ref()
+ .map(|handle| handle.clone_for_wgpu()),
+ };
+ wgpu::util::new_instance_with_webgpu_detection(descriptor).await
}
Self::Existing(existing) => existing.instance.clone(),
}
@@ -98,18 +156,35 @@ pub type NativeAdapterSelectorMethod = Arc<
/// Configuration for creating a new wgpu setup.
///
/// Used for [`WgpuSetup::CreateNew`].
+///
+/// Prefer [`Self::from_display_handle`] when you have a display handle available.
+/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
+/// require it, so providing one ensures maximum compatibility.
+/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
+///
+/// Note: The display handle is stored in [`Self::display_handle`] rather than in
+/// [`Self::instance_descriptor`] so the config can be cloned
+/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
pub struct WgpuSetupCreateNew {
- /// Instance descriptor for creating a wgpu instance.
+ /// Descriptor for the wgpu instance.
///
- /// The most important field is [`wgpu::InstanceDescriptor::backends`], which
- /// controls which backends are supported (wgpu will pick one of these).
- /// If you only want to support WebGL (and not WebGPU),
- /// you can set this to [`wgpu::Backends::GL`].
- /// By default on web, WebGPU will be used if available.
- /// WebGL will only be used as a fallback,
- /// and only if you have enabled the `webgl` feature of crate `wgpu`.
+ /// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
+ /// instead (injected at instance creation time).
+ ///
+ /// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
+ /// which backends are supported (wgpu will pick one of these). For example, set it to
+ /// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
+ /// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
pub instance_descriptor: wgpu::InstanceDescriptor,
+ /// Display handle passed to wgpu at instance creation time.
+ ///
+ /// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
+ /// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
+ ///
+ /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
+ pub display_handle: Option>,
+
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
pub power_preference: wgpu::PowerPreference,
@@ -128,32 +203,37 @@ pub struct WgpuSetupCreateNew {
Arc wgpu::DeviceDescriptor<'static> + Send + Sync>,
}
-impl Clone for WgpuSetupCreateNew {
- fn clone(&self) -> Self {
+impl WgpuSetupCreateNew {
+ /// Creates a new configuration with the given display handle.
+ ///
+ /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
+ /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
+ /// one. Providing it unconditionally ensures your app works everywhere.
+ ///
+ /// If you don't have a display handle available, use [`Self::without_display_handle`]
+ /// instead — it will still work on the majority of platforms.
+ ///
+ /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
+ pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self {
- instance_descriptor: self.instance_descriptor.clone(),
- power_preference: self.power_preference,
- native_adapter_selector: self.native_adapter_selector.clone(),
- device_descriptor: Arc::clone(&self.device_descriptor),
+ display_handle: Some(Box::new(display_handle)),
+ ..Self::without_display_handle()
}
}
-}
-impl std::fmt::Debug for WgpuSetupCreateNew {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("WgpuSetupCreateNew")
- .field("instance_descriptor", &self.instance_descriptor)
- .field("power_preference", &self.power_preference)
- .field(
- "native_adapter_selector",
- &self.native_adapter_selector.is_some(),
- )
- .finish()
- }
-}
-
-impl Default for WgpuSetupCreateNew {
- fn default() -> Self {
+ /// Creates a new configuration without a display handle.
+ ///
+ /// A display handle is not required for headless operation (offscreen rendering, tests,
+ /// compute-only workloads). It also isn't needed on most platforms even when presenting
+ /// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
+ ///
+ /// If you do have a display handle available, prefer [`Self::from_display_handle`] for
+ /// maximum compatibility.
+ ///
+ /// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
+ ///
+ /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
+ pub fn without_display_handle() -> Self {
Self {
instance_descriptor: wgpu::InstanceDescriptor {
// Add GL backend, primarily because WebGPU is not stable enough yet.
@@ -163,8 +243,11 @@ impl Default for WgpuSetupCreateNew {
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
+ display: None,
},
+ display_handle: None,
+
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
@@ -192,6 +275,39 @@ impl Default for WgpuSetupCreateNew {
}
}
+impl Clone for WgpuSetupCreateNew {
+ fn clone(&self) -> Self {
+ let desc = &self.instance_descriptor;
+ Self {
+ instance_descriptor: wgpu::InstanceDescriptor {
+ backends: desc.backends,
+ flags: desc.flags,
+ backend_options: desc.backend_options.clone(),
+ memory_budget_thresholds: desc.memory_budget_thresholds,
+ display: None,
+ },
+ display_handle: self.display_handle.clone(),
+ power_preference: self.power_preference,
+ native_adapter_selector: self.native_adapter_selector.clone(),
+ device_descriptor: Arc::clone(&self.device_descriptor),
+ }
+ }
+}
+
+impl std::fmt::Debug for WgpuSetupCreateNew {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("WgpuSetupCreateNew")
+ .field("instance_descriptor", &self.instance_descriptor)
+ .field("display_handle", &self.display_handle)
+ .field("power_preference", &self.power_preference)
+ .field(
+ "native_adapter_selector",
+ &self.native_adapter_selector.is_some(),
+ )
+ .finish()
+ }
+}
+
/// Configuration for using an existing wgpu setup.
///
/// Used for [`WgpuSetup::Existing`].
diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs
index 167d10c79..3f6adfc27 100644
--- a/crates/egui-wgpu/src/winit.rs
+++ b/crates/egui-wgpu/src/winit.rs
@@ -17,6 +17,7 @@ struct SurfaceState {
width: u32,
height: u32,
resizing: bool,
+ needs_reconfigure: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -234,6 +235,7 @@ impl Painter {
height: size.height,
alpha_mode,
resizing: false,
+ needs_reconfigure: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -362,14 +364,13 @@ impl Painter {
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: The cast is checked with if condition. If the used backend is not metal
- // it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
- // This is how wgpu currently exposes this backend-specific flag.
+ // it gracefully fails.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::() {
- let raw =
- std::ptr::from_ref::(&*hal_surface).cast_mut();
-
- (*raw).present_with_transaction = resizing;
+ hal_surface
+ .render_layer()
+ .lock()
+ .setPresentsWithTransaction(resizing);
Self::configure_surface(
state,
@@ -421,13 +422,41 @@ impl Painter {
) -> f32 {
profiling::function_scope!();
+ /// Guard to ensure that commands are always submitted to the renderer queue
+ /// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
+ /// are completed even if we take a codepath which doesn't submit commands and avoids
+ /// internal buffers growing indefinitely.
+ ///
+ /// This may happen, for example, if no output frame is resolved.
+ /// See for full context.
+ struct RendererQueueGuard<'q> {
+ queue: &'q wgpu::Queue,
+ commands_submitted: bool,
+ }
+
+ impl Drop for RendererQueueGuard<'_> {
+ fn drop(&mut self) {
+ // Only submit an empty command buffer array if no commands were
+ // explicitly submitted.
+ if !self.commands_submitted {
+ self.queue.submit([]);
+ }
+ }
+ }
+
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
let Some(render_state) = self.render_state.as_mut() else {
return vsync_sec;
};
- let Some(surface_state) = self.surfaces.get(&viewport_id) else {
+
+ let mut render_queue_guard = RendererQueueGuard {
+ queue: &render_state.queue,
+ commands_submitted: false,
+ };
+
+ let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec;
};
@@ -464,6 +493,11 @@ impl Painter {
)
};
+ if surface_state.needs_reconfigure {
+ Self::configure_surface(surface_state, render_state, &self.configuration);
+ surface_state.needs_reconfigure = false;
+ }
+
let output_frame = {
profiling::scope!("get_current_texture");
// This is what vsync-waiting happens on my Mac.
@@ -474,16 +508,20 @@ impl Painter {
};
let output_frame = match output_frame {
- Ok(frame) => frame,
- Err(err) => match (*self.configuration.on_surface_error)(err) {
- SurfaceErrorAction::RecreateSurface => {
- Self::configure_surface(surface_state, render_state, &self.configuration);
- return vsync_sec;
+ wgpu::CurrentSurfaceTexture::Success(frame) => frame,
+ wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
+ surface_state.needs_reconfigure = true;
+ frame
+ }
+ other => {
+ match (*self.configuration.on_surface_status)(&other) {
+ SurfaceErrorAction::RecreateSurface => {
+ Self::configure_surface(surface_state, render_state, &self.configuration);
+ }
+ SurfaceErrorAction::SkipFrame => {}
}
- SurfaceErrorAction::SkipFrame => {
- return vsync_sec;
- }
- },
+ return vsync_sec;
+ }
};
let mut capture_buffer = None;
@@ -554,6 +592,7 @@ impl Painter {
}),
timestamp_writes: None,
occlusion_query_set: None,
+ multiview_mask: None,
});
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
@@ -590,6 +629,9 @@ impl Painter {
vsync_sec += start.elapsed().as_secs_f32();
};
+ // Ensure that the queue guard does not do unnecessary work when dropped
+ render_queue_guard.commands_submitted = true;
+
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml
index d1b2ab220..dd4aa8f9d 100644
--- a/crates/egui-winit/Cargo.toml
+++ b/crates/egui-winit/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
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"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
@@ -81,6 +81,7 @@ objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
objc2-ui-kit = { workspace = true, features = [
"std",
+ "objc2-core-foundation",
"UIApplication",
"UIGeometry",
"UIResponder",
diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs
index 75d0469ec..2410c3ee6 100644
--- a/crates/egui-winit/src/clipboard.rs
+++ b/crates/egui-winit/src/clipboard.rs
@@ -65,13 +65,12 @@ impl Clipboard {
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
- return match clipboard.load() {
- Ok(text) => Some(text),
+ match clipboard.load() {
+ Ok(text) => return Some(text),
Err(err) => {
log::error!("smithay paste error: {err}");
- None
}
- };
+ }
}
#[cfg(all(
diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs
index 54059cbd6..90f0311d5 100644
--- a/crates/egui-winit/src/lib.rs
+++ b/crates/egui-winit/src/lib.rs
@@ -9,6 +9,9 @@
#![expect(clippy::manual_range_contains)]
+#[cfg(target_os = "windows")]
+use std::collections::HashSet;
+
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
@@ -102,10 +105,16 @@ pub struct State {
has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")]
- accesskit: Option,
+ pub accesskit: Option,
allow_ime: bool,
ime_rect_px: Option,
+
+ /// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
+ /// release events that should be filtered out. See comments in that method
+ /// for details.
+ #[cfg(target_os = "windows")]
+ pressed_processed_physical_keys: HashSet,
}
impl State {
@@ -148,6 +157,8 @@ impl State {
allow_ime: false,
ime_rect_px: None,
+ #[cfg(target_os = "windows")]
+ pressed_processed_physical_keys: HashSet::new(),
};
slf.egui_input
@@ -364,25 +375,33 @@ impl State {
is_synthetic,
..
} => {
- // Winit generates fake "synthetic" KeyboardInput events when the focus
- // is changed to the window, or away from it. Synthetic key presses
- // represent no real key presses and should be ignored.
- // See https://github.com/rust-windowing/winit/issues/3543
if *is_synthetic && event.state == ElementState::Pressed {
+ // Winit generates fake "synthetic" KeyboardInput events when the focus
+ // is changed to the window, or away from it. Synthetic key presses
+ // represent no real key presses and should be ignored.
+ // See https://github.com/rust-windowing/winit/issues/3543
EventResponse {
repaint: true,
consumed: false,
}
} else {
- self.on_keyboard_input(event);
+ let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
- // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
- let consumed = self.egui_ctx.egui_wants_keyboard_input()
- || event.logical_key
- == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
- EventResponse {
- repaint: true,
- consumed,
+ if let Some(response) =
+ self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
+ {
+ response
+ } else {
+ self.on_keyboard_input(event);
+
+ // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
+ let consumed = egui_wants_keyboard_input
+ || event.logical_key
+ == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
+ EventResponse {
+ repaint: true,
+ consumed,
+ }
}
}
}
@@ -526,6 +545,91 @@ impl State {
}
}
+ #[cfg(not(target_os = "windows"))]
+ #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
+ #[inline(always)]
+ fn try_on_ime_processed_keyboard_input(
+ &mut self,
+ _event: &winit::event::KeyEvent,
+ _egui_wants_keyboard_input: bool,
+ ) -> Option {
+ // `KeyboardInput` events processed by the IME are not emitted by
+ // `winit` on non-Windows platforms, so we don't need to do anything
+ // here.
+
+ None
+ }
+
+ #[cfg(target_os = "windows")]
+ #[inline(always)]
+ fn try_on_ime_processed_keyboard_input(
+ &mut self,
+ event: &winit::event::KeyEvent,
+ egui_wants_keyboard_input: bool,
+ ) -> Option {
+ if !self.allow_ime {
+ None
+ } else if event.logical_key == winit::keyboard::NamedKey::Process {
+ // On Windows, the current version of `winit` (0.30.12) has a bug
+ // where `KeyboardInput` events processed by the IME are still
+ // emitted. [^1]
+ //
+ // As a workaround, we detect these events by checking whether their
+ // `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
+ // them out to keep behavior consistent with other platforms.
+ //
+ // `winit::keyboard::NamedKey::Process` is not documented in
+ // `winit`. Reading through its source code, we find that it is
+ // mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
+ // note, Web is the only other platform that also uses it [^3].)
+ // According to Microsoft, “the IME sets the virtual key value
+ // to `VK_PROCESSKEY` after processing a key input message” [^4].
+ // See also [^5].
+ // (I can't find a documentation page dedicated to this value.)
+ //
+ // TODO(umajho): Remove this workaround once the `winit` bug is fixed
+ // and we've updated to a version that includes the fix. NOTE: Don't
+ // forget to also remove the `pressed_processed_physical_keys` field
+ // and its related code.
+ //
+ // [^1]: https://github.com/rust-windowing/winit/issues/4508
+ // [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
+ // [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
+ // [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
+ // [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
+
+ self.pressed_processed_physical_keys
+ .insert(event.physical_key);
+
+ Some(EventResponse {
+ repaint: false,
+ consumed: egui_wants_keyboard_input,
+ })
+ } else if event.state == ElementState::Released
+ && self
+ .pressed_processed_physical_keys
+ .remove(&event.physical_key)
+ {
+ // Unlike key-presses, we can not tell whether a key-release event
+ // is processed by the IME or not by looking at its `logical_key`,
+ // because their `logical_key` is the original value (e.g.
+ // `winit::keyboard::Key::Character(…)`) rather than
+ // `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
+ // (See the screencast for Windows in [^1].)
+ // So we track the physical keys of processed key-presses and
+ // filter out the corresponding key-releases.
+ //
+ // [^1]: https://github.com/rust-windowing/winit/issues/4508
+
+ Some(EventResponse {
+ repaint: false,
+ consumed: egui_wants_keyboard_input,
+ })
+ } else {
+ None
+ }
+ }
+
/// ## NOTE
///
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
@@ -548,23 +652,23 @@ impl State {
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
- /// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` |
- /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
- /// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` |
+ /// | a-macos15-apple_shuangpin | `Preedit("", None)` -> `Commit("测试")` |
+ /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", None)` -> `Commit("测试")` -> `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
+ /// | c-windows11-ms_pinyin | `Preedit("测试", Some(…))` -> `Preedit("", None)` -> `Commit("测试")` -> `Disabled` |
///
- /// #### Situation: pressed backspace to delete the last character in the prediction
+ /// #### Situation: pressed backspace to delete the last character in the composition
///
/// | Setup | Events in Order |
- /// | a-macos15-apple_shuangpin | `Predict("", None)` |
- /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
- /// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` |
+ /// | a-macos15-apple_shuangpin | `Preedit("", None)` |
+ /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
+ /// | c-windows11-ms_pinyin | `Preedit("", Some(0, 0))` -> `Preedit("", None)` -> `Commit("")` -> `Disabled` |
///
- /// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce"
+ /// #### Situation: clicked somewhere else while there is an active composition with the pre-edit text "ce"
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
/// | a-macos15-apple_shuangpin | nothing emitted |
- /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
+ /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Preedit("", Some(0, 0))` (duplicate) -> `Preedit("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | nothing emitted |
fn on_ime(&mut self, ime: &winit::event::Ime) {
// // code for inspecting ime events emitted by winit:
@@ -610,15 +714,26 @@ impl State {
self.ime_event_disable();
}
winit::event::Ime::Preedit(_, None) => {
- // we need to emit this on macOS, since winit doesn't emit
- // `Predict("", Some(0, 0))` before this event on macOS when the
- // user deletes the last character in the prediction with the
- // backspace key. Without this, only `egui::ImeEvent::Disabled`
- // is emitted here, leading to the last character being left in
- // TextEdit in such situation.
- self.egui_input
- .events
- .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
+ if cfg!(target_os = "macos") {
+ // On macOS, when the user presses backspace to delete the
+ // last character in an IME composition, `winit` only emits
+ // `winit::event::Ime::Preedit("", None)` without a
+ // preceding `winit::event::Ime::Preedit("", Some(0, 0))`.
+ //
+ // The current implementation of `egui::TextEdit` relies on
+ // receiving an `egui::ImeEvent::Preedit("")` to remove the
+ // last character in the composition in this case, so we
+ // emit it here.
+ //
+ // This is guarded to macOS-only, as applying it on other
+ // platforms is unnecessary and can cause undesired
+ // behavior.
+ // See: https://github.com/emilk/egui/pull/7973
+ self.egui_input
+ .events
+ .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
+ }
+
self.ime_event_disable();
}
}
@@ -640,11 +755,27 @@ impl State {
self.has_sent_ime_enabled = false;
}
- pub fn on_mouse_motion(&mut self, delta: (f64, f64)) {
+ /// Returns `true` if the event was sent to egui.
+ pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
+ if !self.is_pointer_in_window() && !self.any_pointer_button_down {
+ return false;
+ }
+
self.egui_input.events.push(egui::Event::MouseMoved(Vec2 {
x: delta.0 as f32,
y: delta.1 as f32,
}));
+ true
+ }
+
+ /// Returns `true` when the pointer is currently inside the window.
+ pub fn is_pointer_in_window(&self) -> bool {
+ self.pointer_pos_in_points.is_some()
+ }
+
+ /// Returns `true` if any pointer button is currently held down.
+ pub fn is_any_pointer_button_down(&self) -> bool {
+ self.any_pointer_button_down
}
/// Call this when there is a new [`accesskit::ActionRequest`].
@@ -974,6 +1105,16 @@ impl State {
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
+ #[cfg(target_os = "windows")]
+ if !self.allow_ime {
+ // Defensively clear the set to avoid unexpected behavior.
+ //
+ // We don't do the same in `ime_event_disable` because the key
+ // release events for IME confirmation keys arrive after
+ // `winit::event::Ime::Disabled`.
+ self.pressed_processed_physical_keys.clear();
+ }
+
profiling::scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime);
}
diff --git a/crates/egui-winit/src/safe_area.rs b/crates/egui-winit/src/safe_area.rs
index 5f4a9f9cf..378f44a94 100644
--- a/crates/egui-winit/src/safe_area.rs
+++ b/crates/egui-winit/src/safe_area.rs
@@ -36,8 +36,8 @@ mod ios {
| UISceneActivationState::ForegroundInactive
)
{
- // Safe to cast, the class kind was checked above
- let window_scene = Retained::cast::(scene.clone());
+ // SAFETY: class kind was checked above with `isKindOfClass`
+ let window_scene = Retained::cast_unchecked::(scene.clone());
if let Some(window) = window_scene.keyWindow() {
let insets = window.safeAreaInsets();
return SafeAreaInsets(MarginF32 {
diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml
index 764d2401e..3a22bf529 100644
--- a/crates/egui/Cargo.toml
+++ b/crates/egui/Cargo.toml
@@ -11,7 +11,7 @@ readme = "../../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs
index 6425ac724..6f289fcfb 100644
--- a/crates/egui/src/atomics/atom.rs
+++ b/crates/egui/src/atomics/atom.rs
@@ -1,10 +1,24 @@
-use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui};
-use emath::{NumExt as _, Vec2};
+use crate::{AtomKind, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui};
+use emath::{Align2, 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.
+/// This can be a piece of text, an image, or even a custom widget.
+/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more.
+///
+/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
+///
+/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter,
+/// which allows you to easily create atoms from tuples of text, images, and other atoms:
+/// ```
+/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id};
+/// # egui::__run_test_ui(|ui| {
+/// let image = egui::include_image!("../../../eframe/data/icon.png");
+/// ui.button((image, "Click me!"));
+/// # });
+/// ```
+///
/// You can directly call the `atom_*` methods on anything that implements `Into`.
/// ```
/// # use egui::{Image, emath::Vec2};
@@ -14,6 +28,9 @@ use epaint::text::TextWrapMode;
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
+ /// See [`crate::AtomExt::atom_id`]
+ pub id: Option,
+
/// See [`crate::AtomExt::atom_size`]
pub size: Option,
@@ -26,17 +43,22 @@ pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,
- /// The atom type
+ /// See [`crate::AtomExt::atom_align`]
+ pub align: Align2,
+
+ /// The atom type / content
pub kind: AtomKind<'a>,
}
impl Default for Atom<'_> {
fn default() -> Self {
Atom {
+ id: None,
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
+ align: Align2::CENTER_CENTER,
kind: AtomKind::Empty,
}
}
@@ -54,11 +76,27 @@ impl<'a> Atom<'a> {
}
}
- /// Create a [`AtomKind::Custom`] with a specific size.
+ /// Create an [`AtomKind::Empty`] with a specific size.
+ ///
+ /// Example:
+ /// ```
+ /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
+ /// # use emath::Vec2;
+ /// # __run_test_ui(|ui| {
+ /// let id = Id::new("my_button");
+ /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
+ ///
+ /// let rect = response.rect(id);
+ /// if let Some(rect) = rect {
+ /// ui.place(rect, Button::new("⏵"));
+ /// }
+ /// # });
+ /// ```
pub fn custom(id: Id, size: impl Into) -> Self {
Atom {
size: Some(size.into()),
- kind: AtomKind::Custom(id),
+ kind: AtomKind::Empty,
+ id: Some(id),
..Default::default()
}
}
@@ -82,19 +120,32 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate);
}
- let (intrinsic, kind) = self
- .kind
- .into_sized(ui, available_size, wrap_mode, fallback_font);
+ let id = self.id;
+
+ let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
+ let IntoSizedResult {
+ intrinsic_size,
+ sized,
+ } = self.kind.into_sized(
+ ui,
+ IntoSizedArgs {
+ available_size,
+ wrap_mode,
+ fallback_font,
+ },
+ );
let size = self
.size
- .map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
+ .map_or_else(|| sized.size(), |s| s.at_most(self.max_size));
SizedAtom {
+ id,
size,
- intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
+ intrinsic_size: intrinsic_size.at_least(self.size.unwrap_or_default()),
grow: self.grow,
- kind,
+ align: self.align,
+ kind: sized,
}
}
}
diff --git a/crates/egui/src/atomics/atom_ext.rs b/crates/egui/src/atomics/atom_ext.rs
index 6d008b84b..bfe587fae 100644
--- a/crates/egui/src/atomics/atom_ext.rs
+++ b/crates/egui/src/atomics/atom_ext.rs
@@ -1,10 +1,16 @@
-use crate::{Atom, FontSelection, Ui};
+use crate::{Atom, FontSelection, Id, 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 [`Id`] for custom rendering.
+ ///
+ /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
+ /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
+ fn atom_id(self, id: Id) -> Atom<'a>;
+
/// Set the atom to a fixed size.
///
/// If [`Atom::grow`] is `true`, this will be the minimum width.
@@ -63,12 +69,23 @@ pub trait AtomExt<'a> {
let height = ui.fonts_mut(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
+
+ /// Sets the [`emath::Align2`] of a single atom within its available space.
+ ///
+ /// Defaults to center-center.
+ fn atom_align(self, align: emath::Align2) -> Atom<'a>;
}
impl<'a, T> AtomExt<'a> for T
where
T: Into> + Sized,
{
+ fn atom_id(self, id: Id) -> Atom<'a> {
+ let mut atom = self.into();
+ atom.id = Some(id);
+ atom
+ }
+
fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.size = Some(size);
@@ -104,4 +121,10 @@ where
atom.max_size.y = max_height;
atom
}
+
+ fn atom_align(self, align: emath::Align2) -> Atom<'a> {
+ let mut atom = self.into();
+ atom.align = align;
+ atom
+ }
}
diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs
index 10ca3353b..ec2ab8f63 100644
--- a/crates/egui/src/atomics/atom_kind.rs
+++ b/crates/egui/src/atomics/atom_kind.rs
@@ -1,9 +1,28 @@
-use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
+use crate::{FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use emath::Vec2;
use epaint::text::TextWrapMode;
+use std::fmt::Debug;
+
+/// Args passed when sizing an [`super::Atom`]
+pub struct IntoSizedArgs {
+ pub available_size: Vec2,
+ pub wrap_mode: TextWrapMode,
+ pub fallback_font: FontSelection,
+}
+
+/// Result returned when sizing an [`super::Atom`]
+pub struct IntoSizedResult<'a> {
+ pub intrinsic_size: Vec2,
+ pub sized: SizedAtomKind<'a>,
+}
+
+/// See [`AtomKind::Closure`]
+// We need 'static in the result (or need to introduce another lifetime on the enum).
+// Otherwise, a single 'static Atom would force the closure to be 'static.
+pub type AtomClosure<'a> = Box IntoSizedResult<'static> + 'a>;
/// The different kinds of [`crate::Atom`]s.
-#[derive(Clone, Default, Debug)]
+#[derive(Default)]
pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default]
@@ -38,37 +57,57 @@ pub enum AtomKind<'a> {
/// default font height, which is convenient for icons.
Image(Image<'a>),
- /// For custom rendering.
+ /// A custom closure that produces a sized atom.
///
- /// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
- /// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
+ /// The vec2 passed in is the available size to this atom. The returned vec2 should be the
+ /// preferred / intrinsic size.
///
- /// Example:
- /// ```
- /// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
- /// # use emath::Vec2;
- /// # __run_test_ui(|ui| {
- /// let id = Id::new("my_button");
- /// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
- ///
- /// let rect = response.rect(id);
- /// if let Some(rect) = rect {
- /// ui.place(rect, Button::new("⏵"));
- /// }
- /// # });
- /// ```
- Custom(Id),
+ /// Note: This api is experimental, expect breaking changes here.
+ /// When cloning, this will be cloned as [`AtomKind::Empty`].
+ Closure(AtomClosure<'a>),
+}
+
+impl Clone for AtomKind<'_> {
+ fn clone(&self) -> Self {
+ match self {
+ AtomKind::Empty => AtomKind::Empty,
+ AtomKind::Text(text) => AtomKind::Text(text.clone()),
+ AtomKind::Image(image) => AtomKind::Image(image.clone()),
+ AtomKind::Closure(_) => {
+ log::warn!("Cannot clone atom closures");
+ AtomKind::Empty
+ }
+ }
+ }
+}
+
+impl Debug for AtomKind<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ AtomKind::Empty => write!(f, "AtomKind::Empty"),
+ AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
+ AtomKind::Image(image) => write!(f, "AtomKind::Image({image:?})"),
+ AtomKind::Closure(_) => write!(f, "AtomKind::Closure()"),
+ }
+ }
}
impl<'a> AtomKind<'a> {
+ /// See [`Self::Text`]
pub fn text(text: impl Into) -> Self {
AtomKind::Text(text.into())
}
+ /// See [`Self::Image`]
pub fn image(image: impl Into>) -> Self {
AtomKind::Image(image.into())
}
+ /// See [`Self::Closure`]
+ pub fn closure(func: impl FnOnce(&Ui, IntoSizedArgs) -> IntoSizedResult<'static> + 'a) -> Self {
+ AtomKind::Closure(Box::new(func))
+ }
+
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
///
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
@@ -76,23 +115,40 @@ impl<'a> AtomKind<'a> {
pub fn into_sized(
self,
ui: &Ui,
- available_size: Vec2,
- wrap_mode: Option,
- fallback_font: FontSelection,
- ) -> (Vec2, SizedAtomKind<'a>) {
+ IntoSizedArgs {
+ available_size,
+ wrap_mode,
+ fallback_font,
+ }: IntoSizedArgs,
+ ) -> IntoSizedResult<'a> {
match self {
AtomKind::Text(text) => {
- let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
- (galley.intrinsic_size(), SizedAtomKind::Text(galley))
+ IntoSizedResult {
+ intrinsic_size: galley.intrinsic_size(),
+ sized: 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))
+ IntoSizedResult {
+ intrinsic_size: size,
+ sized: SizedAtomKind::Image { image, size },
+ }
}
- AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
- AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
+ AtomKind::Empty => IntoSizedResult {
+ intrinsic_size: Vec2::ZERO,
+ sized: SizedAtomKind::Empty { size: None },
+ },
+ AtomKind::Closure(func) => func(
+ ui,
+ IntoSizedArgs {
+ available_size,
+ wrap_mode,
+ fallback_font,
+ },
+ ),
}
}
}
diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs
index 8132a7dc9..7894273f3 100644
--- a/crates/egui/src/atomics/atom_layout.rs
+++ b/crates/egui/src/atomics/atom_layout.rs
@@ -38,6 +38,7 @@ pub struct AtomLayout<'a> {
fallback_text_color: Option,
fallback_font: Option,
min_size: Vec2,
+ max_size: Vec2,
wrap_mode: Option,
align2: Option,
}
@@ -59,6 +60,7 @@ impl<'a> AtomLayout<'a> {
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
+ max_size: Vec2::INFINITY,
wrap_mode: None,
align2: None,
}
@@ -113,6 +115,33 @@ impl<'a> AtomLayout<'a> {
self
}
+ /// Set the maximum size of the Widget.
+ ///
+ /// By default, the size is limited by the available size in the [`Ui`].
+ #[inline]
+ pub fn max_size(mut self, size: Vec2) -> Self {
+ self.max_size = size;
+ self
+ }
+
+ /// Set the maximum width of the Widget.
+ ///
+ /// By default, the width is limited by the available width in the [`Ui`].
+ #[inline]
+ pub fn max_width(mut self, width: f32) -> Self {
+ self.max_size.x = width;
+ self
+ }
+
+ /// Set the maximum height of the Widget.
+ ///
+ /// By default, the height is limited by the available height in the [`Ui`].
+ #[inline]
+ pub fn max_height(mut self, height: f32) -> Self {
+ self.max_size.y = height;
+ self
+ }
+
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
@@ -161,6 +190,7 @@ impl<'a> AtomLayout<'a> {
sense,
fallback_text_color,
min_size,
+ mut max_size,
wrap_mode,
align2,
fallback_font,
@@ -173,7 +203,7 @@ impl<'a> AtomLayout<'a> {
// 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);
+ let any_shrink = atoms.any_shrink();
if !any_shrink {
let first_text = atoms
.iter_mut()
@@ -190,8 +220,16 @@ impl<'a> AtomLayout<'a> {
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
+ // max_size has no effect in justified layouts. If we'd limit the available size here,
+ // the content would be sized differently than the frame which would look weird.
+ if ui.layout().horizontal_justify() {
+ max_size.x = f32::INFINITY;
+ }
+
+ let available_size = ui.available_size().at_most(max_size);
+
// The size available for the content
- let available_inner_size = ui.available_size() - frame.total_margin().sum();
+ let available_inner_size = available_size - frame.total_margin().sum();
let mut desired_width = 0.0;
@@ -280,8 +318,9 @@ impl<'a> AtomLayout<'a> {
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
- response.intrinsic_size =
- Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
+ response.set_intrinsic_size(
+ (Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size),
+ );
AllocatedAtomLayout {
sized_atoms: sized_items,
@@ -321,7 +360,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images(&self) -> impl Iterator- > {
self.iter_kinds().filter_map(|kind| {
- if let SizedAtomKind::Image(image, _) = kind {
+ if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
@@ -331,7 +370,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_images_mut(&mut self) -> impl Iterator
- > {
self.iter_kinds_mut().filter_map(|kind| {
- if let SizedAtomKind::Image(image, _) = kind {
+ if let SizedAtomKind::Image { image, size: _ } = kind {
Some(image)
} else {
None
@@ -373,8 +412,11 @@ impl<'atom> AllocatedAtomLayout<'atom> {
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
- if let SizedAtomKind::Image(image, size) = kind {
- SizedAtomKind::Image(f(image), size)
+ if let SizedAtomKind::Image { image, size } = kind {
+ SizedAtomKind::Image {
+ image: f(image),
+ size,
+ }
} else {
kind
}
@@ -422,25 +464,24 @@ impl<'atom> AllocatedAtomLayout<'atom> {
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
+ let rect = sized.align.align_size_within_rect(size, frame);
- let align = Align2::CENTER_CENTER;
- let rect = align.align_size_within_rect(size, frame);
+ if let Some(id) = sized.id {
+ debug_assert!(
+ !response.custom_rects.iter().any(|(i, _)| *i == id),
+ "Duplicate custom id"
+ );
+ response.custom_rects.push((id, rect));
+ }
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
- SizedAtomKind::Image(image, _) => {
+ SizedAtomKind::Image { image, size: _ } => {
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 => {}
+ SizedAtomKind::Empty { .. } => {}
}
}
@@ -450,7 +491,7 @@ impl<'atom> AllocatedAtomLayout<'atom> {
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
///
-/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
+/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`crate::Atom::custom`].
#[derive(Clone, Debug)]
pub struct AtomLayoutResponse {
pub response: Response,
@@ -470,7 +511,7 @@ impl AtomLayoutResponse {
self.custom_rects.iter().copied()
}
- /// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
+ /// Use this together with [`crate::Atom::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 {
@@ -480,6 +521,20 @@ impl AtomLayoutResponse {
}
}
+impl Deref for AtomLayoutResponse {
+ type Target = Response;
+
+ fn deref(&self) -> &Self::Target {
+ &self.response
+ }
+}
+
+impl DerefMut for AtomLayoutResponse {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.response
+ }
+}
+
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs
index 1db7c63c6..761db8eb6 100644
--- a/crates/egui/src/atomics/atoms.rs
+++ b/crates/egui/src/atomics/atoms.rs
@@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut};
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
+///
+/// Many widgets take an `impl` [`IntoAtoms`] parameter,
+/// which allows you to easily create atoms from tuples of text, images, and other atoms:
+/// ```
+/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
+/// # egui::__run_test_ui(|ui| {
+/// let image = egui::include_image!("../../../eframe/data/icon.png");
+/// ui.button((image, "Click me!"));
+/// # });
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
@@ -21,11 +30,26 @@ impl<'a> Atoms<'a> {
self.0.push(atom.into());
}
+ /// Extend the list of atoms by appending more atoms to the right side.
+ ///
+ /// If you have weird lifetime issues with this, use [`Self::push_right`] in a loop instead.
+ pub fn extend_right(&mut self, atoms: Self) {
+ self.0.extend(atoms.0);
+ }
+
/// Insert a new [`Atom`] at the beginning of the list (left side).
pub fn push_left(&mut self, atom: impl Into>) {
self.0.insert(0, atom.into());
}
+ /// Extend the list of atoms by prepending more atoms to the left side.
+ ///
+ /// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead.
+ pub fn extend_left(&mut self, mut atoms: Self) {
+ std::mem::swap(&mut atoms.0, &mut self.0);
+ self.0.extend(atoms.0);
+ }
+
/// 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.
@@ -54,6 +78,11 @@ impl<'a> Atoms<'a> {
string
}
+ /// Do any of the atoms have shrink set to `true`?
+ pub fn any_shrink(&self) -> bool {
+ self.iter().any(|a| a.shrink)
+ }
+
pub fn iter_kinds(&self) -> impl Iterator
- > {
self.0.iter().map(|atom| &atom.kind)
}
@@ -172,6 +201,16 @@ where
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
+///
+/// Many widgets take an `impl` [`IntoAtoms`] parameter,
+/// which allows you to easily create atoms from tuples of text, images, and other atoms:
+/// ```
+/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
+/// # egui::__run_test_ui(|ui| {
+/// let image = egui::include_image!("../../../eframe/data/icon.png");
+/// ui.button((image, "Click me!"));
+/// # });
+/// ```
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);
diff --git a/crates/egui/src/atomics/sized_atom.rs b/crates/egui/src/atomics/sized_atom.rs
index f1ae0f81b..19c289ab3 100644
--- a/crates/egui/src/atomics/sized_atom.rs
+++ b/crates/egui/src/atomics/sized_atom.rs
@@ -4,6 +4,8 @@ use emath::Vec2;
/// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)]
pub struct SizedAtom<'a> {
+ pub id: Option,
+
pub(crate) grow: bool,
/// The size of the atom.
@@ -15,6 +17,9 @@ pub struct SizedAtom<'a> {
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2,
+ /// How will the atom be aligned in its available space?
+ pub align: emath::Align2,
+
pub kind: SizedAtomKind<'a>,
}
diff --git a/crates/egui/src/atomics/sized_atom_kind.rs b/crates/egui/src/atomics/sized_atom_kind.rs
index ff8da1631..02263adad 100644
--- a/crates/egui/src/atomics/sized_atom_kind.rs
+++ b/crates/egui/src/atomics/sized_atom_kind.rs
@@ -1,16 +1,20 @@
-use crate::{Id, Image};
+use crate::Image;
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
/// A sized [`crate::AtomKind`].
-#[derive(Clone, Default, Debug)]
+#[derive(Clone, Debug)]
pub enum SizedAtomKind<'a> {
- #[default]
- Empty,
+ Empty { size: Option },
Text(Arc),
- Image(Image<'a>, Vec2),
- Custom(Id),
+ Image { image: Image<'a>, size: Vec2 },
+}
+
+impl Default for SizedAtomKind<'_> {
+ fn default() -> Self {
+ Self::Empty { size: None }
+ }
}
impl SizedAtomKind<'_> {
@@ -18,8 +22,8 @@ impl SizedAtomKind<'_> {
pub fn size(&self) -> Vec2 {
match self {
SizedAtomKind::Text(galley) => galley.size(),
- SizedAtomKind::Image(_, size) => *size,
- SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
+ SizedAtomKind::Image { image: _, size } => *size,
+ SizedAtomKind::Empty { size } => size.unwrap_or_default(),
}
}
}
diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs
index 4333cf73a..d44c0ae41 100644
--- a/crates/egui/src/containers/area.rs
+++ b/crates/egui/src/containers/area.rs
@@ -516,6 +516,7 @@ impl Area {
let move_response = ctx.create_widget(
WidgetRect {
id: interact_id,
+ parent_id: id,
layer_id,
rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),
diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs
index 1bd5954c8..cfdaac827 100644
--- a/crates/egui/src/containers/menu.rs
+++ b/crates/egui/src/containers/menu.rs
@@ -10,7 +10,7 @@
use crate::style::StyleModifier;
use crate::{
- Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
+ Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{Align, RectAlign, Vec2, vec2};
@@ -458,6 +458,7 @@ impl SubMenu {
let is_any_open = open_item.is_some();
let mut is_open = open_item == Some(id);
+ let was_open = is_open;
let mut set_open = None;
// We expand the button rect so there is no empty space where no menu is shown
@@ -470,9 +471,21 @@ impl SubMenu {
// But since we check if no other menu is open, nothing should be able to cover the button
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
+ // `clicked` includes keyboard and accessibility click actions.
+ // We want Enter/Space to toggle an already open submenu, while pointer clicks should keep
+ // the submenu open (for touch and pointer interactions).
+ let clicked = button_response.clicked();
+ let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary);
+ let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer;
+
+ if ui.is_enabled() && is_open && clicked_by_keyboard_or_access {
+ set_open = Some(false);
+ is_open = false;
+ }
+
// The clicked handler is there for accessibility (keyboard navigation)
let should_open =
- ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
+ ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open));
if should_open {
set_open = Some(true);
is_open = true;
diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs
index f2a4c3b67..4a83ce8d1 100644
--- a/crates/egui/src/containers/panel.rs
+++ b/crates/egui/src/containers/panel.rs
@@ -561,7 +561,11 @@ impl Panel {
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate
- let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?;
+ let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else {
+ // Make sure the ids of the next widgets are the same whether we show the panel or not:
+ ui.skip_ahead_auto_ids(1);
+ return None;
+ };
if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state:
diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs
index c6b739589..ae5fbfc8f 100644
--- a/crates/egui/src/containers/window.rs
+++ b/crates/egui/src/containers/window.rs
@@ -962,6 +962,7 @@ fn do_resize_interaction(
WidgetRect {
layer_id,
id,
+ parent_id: layer_id.id,
rect,
interact_rect: rect,
sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs
index 2665d5edd..51a663ce5 100644
--- a/crates/egui/src/context.rs
+++ b/crates/egui/src/context.rs
@@ -1360,6 +1360,7 @@ impl Context {
let WidgetRect {
id,
+ parent_id: _,
layer_id,
rect,
interact_rect,
@@ -1378,8 +1379,8 @@ impl Context {
interact_rect,
sense,
flags: Flags::empty(),
- interact_pointer_pos: None,
- intrinsic_size: None,
+ interact_pointer_pos_or_nan: Pos2::NAN,
+ intrinsic_size_or_nan: Vec2::NAN,
};
res.flags.set(Flags::ENABLED, enabled);
@@ -1470,14 +1471,11 @@ impl Context {
|| res.long_touched()
|| clicked
|| res.drag_stopped();
- if is_interacted_with {
- res.interact_pointer_pos = input.pointer.interact_pos();
- if let (Some(to_global), Some(pos)) = (
- memory.to_global.get(&res.layer_id),
- &mut res.interact_pointer_pos,
- ) {
- *pos = to_global.inverse() * *pos;
+ if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() {
+ if let Some(to_global) = memory.to_global.get(&res.layer_id) {
+ pos = to_global.inverse() * pos;
}
+ res.interact_pointer_pos_or_nan = pos;
}
if input.pointer.any_down() && !is_interacted_with {
@@ -2397,6 +2395,12 @@ impl Context {
crate::gui_zoom::zoom_with_keyboard(self);
}
+ for shortcut in self.options(|o| o.quit_shortcuts.clone()) {
+ if self.input_mut(|i| i.consume_shortcut(&shortcut)) {
+ self.send_viewport_cmd(ViewportCommand::Close);
+ }
+ }
+
#[cfg(debug_assertions)]
self.debug_painting();
@@ -2616,6 +2620,7 @@ impl ContextImpl {
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
+ tree_id: accesskit::TreeId::ROOT,
focus: focus_id,
});
}
@@ -2635,6 +2640,19 @@ impl ContextImpl {
}
}
+ #[cfg(debug_assertions)]
+ let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id {
+ let mut shapes = shapes;
+ warn_if_rect_changes_id(
+ &mut shapes,
+ &viewport.prev_pass.widgets,
+ &viewport.this_pass.widgets,
+ );
+ shapes
+ } else {
+ shapes
+ };
+
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
if repaint_needed {
@@ -3262,7 +3280,7 @@ impl Context {
for (name, data) in &mut font_definitions.font_data {
ui.collapsing(name, |ui| {
- let mut tweak = data.tweak;
+ let mut tweak = data.tweak.clone();
if tweak.ui(ui).changed() {
Arc::make_mut(data).tweak = tweak;
changed = true;
@@ -4236,6 +4254,112 @@ fn context_impl_send_sync() {
assert_send_sync::();
}
+/// Check if any [`Rect`] appears with different [`Id`]s between two passes.
+///
+/// This helps detect cases where the same screen area is claimed by different widget ids
+/// across passes, which is often a sign of id instability.
+#[cfg(debug_assertions)]
+fn warn_if_rect_changes_id(
+ out_shapes: &mut Vec,
+ prev_widgets: &crate::WidgetRects,
+ new_widgets: &crate::WidgetRects,
+) {
+ profiling::function_scope!();
+
+ use std::collections::BTreeMap;
+
+ /// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats.
+ #[derive(Clone, Copy, PartialEq, Eq)]
+ struct OrderedRect(Rect);
+
+ impl PartialOrd for OrderedRect {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+ }
+
+ impl Ord for OrderedRect {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ let lhs = self.0;
+ let rhs = other.0;
+ lhs.min
+ .x
+ .to_bits()
+ .cmp(&rhs.min.x.to_bits())
+ .then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits()))
+ .then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits()))
+ .then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits()))
+ }
+ }
+
+ fn create_lookup<'a>(
+ widgets: impl Iterator
- ,
+ ) -> BTreeMap> {
+ let mut lookup: BTreeMap> = BTreeMap::default();
+ for w in widgets {
+ lookup.entry(OrderedRect(w.rect)).or_default().push(w);
+ }
+ lookup
+ }
+
+ for (layer_id, new_layer_widgets) in new_widgets.layers() {
+ let prev = create_lookup(prev_widgets.get_layer(*layer_id));
+ let new = create_lookup(new_layer_widgets.iter());
+
+ for (hashable_rect, new_at_rect) in new {
+ let Some(prev_at_rect) = prev.get(&hashable_rect) else {
+ continue; // this rect did not exist in the previous pass
+ };
+
+ if prev_at_rect
+ .iter()
+ .any(|w| new_at_rect.iter().any(|nw| nw.id == w.id))
+ {
+ continue; // at least one id stayed the same, so this is not an id change
+ }
+
+ // Only warn if at least one of the previous ids is gone from this layer entirely.
+ // If they all still exist (just at a different rect), then the rect match
+ // is just a coincidence caused by widgets shifting (e.g. a window being dragged).
+ if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) {
+ continue;
+ }
+
+ // Only warn if at least one widget has the same parent_id in both frames.
+ // If all parent_ids changed too, this is a cascading id shift, not a widget bug.
+ if !prev_at_rect
+ .iter()
+ .any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id))
+ {
+ continue;
+ }
+
+ let rect = new_at_rect[0].rect;
+
+ log::warn!(
+ "Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}",
+ prev_at_rect
+ .iter()
+ .map(|w| w.id.short_debug_format())
+ .collect::>(),
+ new_at_rect
+ .iter()
+ .map(|w| w.id.short_debug_format())
+ .collect::>(),
+ );
+ out_shapes.push(ClippedShape {
+ clip_rect: Rect::EVERYTHING,
+ shape: epaint::Shape::rect_stroke(
+ rect,
+ 0,
+ (2.0, Color32::RED),
+ StrokeKind::Outside,
+ ),
+ });
+ }
+ }
+}
+
#[cfg(test)]
mod test {
use super::Context;
diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs
index 02e9bc6b7..00d06d465 100644
--- a/crates/egui/src/data/input.rs
+++ b/crates/egui/src/data/input.rs
@@ -253,9 +253,28 @@ pub struct ViewportInfo {
///
/// This should be the same as [`RawInput::focused`].
pub focused: Option,
+
+ /// Is the window fully occluded (completely covered) by another window?
+ ///
+ /// Not all platforms support this.
+ /// On platforms that don't, this will be `None` or `Some(false)`.
+ pub occluded: Option,
}
impl ViewportInfo {
+ /// Is the window considered visible for rendering purposes?
+ ///
+ /// A window is not visible if it is minimized or occluded.
+ /// When not visible, the UI is not painted and rendering is skipped,
+ /// but application logic may still be executed by some integrations.
+ pub fn visible(&self) -> Option {
+ match (self.minimized, self.occluded) {
+ (Some(true), _) | (_, Some(true)) => Some(false),
+ (Some(false), Some(false)) => Some(true),
+ (_, None) | (None, _) => None,
+ }
+ }
+
/// This viewport has been told to close.
///
/// If this is the root viewport, the application will exit
@@ -282,6 +301,7 @@ impl ViewportInfo {
maximized: self.maximized,
fullscreen: self.fullscreen,
focused: self.focused,
+ occluded: self.occluded,
}
}
@@ -298,6 +318,7 @@ impl ViewportInfo {
maximized,
fullscreen,
focused,
+ occluded,
} = self;
crate::Grid::new("viewport_info").show(ui, |ui| {
@@ -345,6 +366,16 @@ impl ViewportInfo {
ui.label(opt_as_str(focused));
ui.end_row();
+ ui.label("Occluded:");
+ ui.label(opt_as_str(occluded));
+ ui.end_row();
+
+ let visible = self.visible();
+
+ ui.label("Visible:");
+ ui.label(opt_as_str(&visible));
+ ui.end_row();
+
fn opt_rect_as_string(v: &Option) -> String {
v.as_ref().map_or(String::new(), |r| {
format!("Pos: {:?}, size: {:?}", r.min, r.size())
@@ -410,6 +441,10 @@ pub enum Event {
Text(String),
/// A key was pressed or released.
+ ///
+ /// ## Note for integration authors
+ ///
+ /// Key events that has been processed by IMEs should not be sent to `egui`.
Key {
/// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak
/// keyboard layout, it will be taken into account.
diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs
index 8fe962b36..c7ffd7cda 100644
--- a/crates/egui/src/hit_test.rs
+++ b/crates/egui/src/hit_test.rs
@@ -450,6 +450,7 @@ mod tests {
fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect {
WidgetRect {
id,
+ parent_id: Id::NULL,
layer_id: LayerId::background(),
rect,
interact_rect: rect,
diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs
index 7484930c8..661bdf2bf 100644
--- a/crates/egui/src/id.rs
+++ b/crates/egui/src/id.rs
@@ -79,7 +79,7 @@ impl Id {
self.0.get()
}
- pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
+ pub fn accesskit_id(&self) -> accesskit::NodeId {
self.value().into()
}
diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs
index 37faf64c2..b32a75c57 100644
--- a/crates/egui/src/input_state/mod.rs
+++ b/crates/egui/src/input_state/mod.rs
@@ -661,6 +661,8 @@ impl InputState {
if self.pointer.wants_repaint()
|| self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2
|| !self.events.is_empty()
+ || !self.raw.hovered_files.is_empty()
+ || !self.raw.dropped_files.is_empty()
{
// Immediate repaint
return Some(Duration::ZERO);
@@ -869,7 +871,8 @@ impl InputState {
let accesskit_id = id.accesskit_id();
self.events.iter().filter_map(move |event| {
if let Event::AccessKitActionRequest(request) = event
- && request.target == accesskit_id
+ && request.target_node == accesskit_id
+ && request.target_tree == accesskit::TreeId::ROOT
&& request.action == action
{
return Some(request);
@@ -886,7 +889,8 @@ impl InputState {
let accesskit_id = id.accesskit_id();
self.events.retain(|event| {
if let Event::AccessKitActionRequest(request) = event
- && request.target == accesskit_id
+ && request.target_node == accesskit_id
+ && request.target_tree == accesskit::TreeId::ROOT
{
return !consume(request);
}
diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs
index c44c928bb..c35fd254b 100644
--- a/crates/egui/src/layout.rs
+++ b/crates/egui/src/layout.rs
@@ -623,12 +623,24 @@ impl Layout {
if (self.is_vertical() && self.horizontal_align() == Align::Center)
|| self.horizontal_justify()
{
- frame_size.x = frame_size.x.max(available_rect.width()); // fill full width
+ // For wrapping layouts, fill the current column width, not the entire layout width.
+ let width = if self.main_wrap {
+ region.cursor.width()
+ } else {
+ available_rect.width()
+ };
+ frame_size.x = frame_size.x.max(width); // fill full width
}
if (self.is_horizontal() && self.vertical_align() == Align::Center)
|| self.vertical_justify()
{
- frame_size.y = frame_size.y.max(available_rect.height()); // fill full height
+ // For wrapping layouts, fill the current row height, not the entire layout height.
+ let height = if self.main_wrap {
+ region.cursor.height()
+ } else {
+ available_rect.height()
+ };
+ frame_size.y = frame_size.y.max(height); // fill full height
}
let align2 = match self.main_dir {
@@ -791,14 +803,14 @@ impl Layout {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(region.max_rect.left(), new_top),
- pos2(INFINITY, new_top + region.cursor.height()),
+ pos2(INFINITY, new_top),
);
}
Direction::RightToLeft => {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(-INFINITY, new_top),
- pos2(region.max_rect.right(), new_top + region.cursor.height()),
+ pos2(region.max_rect.right(), new_top),
);
}
Direction::TopDown | Direction::BottomUp => {}
diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs
index bc90f0cf9..d86851a1d 100644
--- a/crates/egui/src/lib.rs
+++ b/crates/egui/src/lib.rs
@@ -685,7 +685,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
}
/// For use in tests; especially doctests.
-pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
+pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| {
diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs
index fbc8e6f68..08b08a462 100644
--- a/crates/egui/src/memory/mod.rs
+++ b/crates/egui/src/memory/mod.rs
@@ -234,6 +234,16 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub zoom_with_keyboard: bool,
+ /// Keyboard shortcuts to close the application.
+ ///
+ /// Pressing any of these will send [`crate::ViewportCommand::Close`]
+ /// to the root viewport.
+ ///
+ /// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac).
+ /// Set to empty to disable.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ pub quit_shortcuts: Vec,
+
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
@@ -304,6 +314,10 @@ impl Default for Options {
system_theme: None,
zoom_factor: 1.0,
zoom_with_keyboard: true,
+ quit_shortcuts: vec![crate::KeyboardShortcut::new(
+ crate::Modifiers::COMMAND,
+ crate::Key::Q,
+ )],
tessellation_options: Default::default(),
repaint_on_widget_change: false,
@@ -363,6 +377,7 @@ impl Options {
system_theme: _,
zoom_factor,
zoom_with_keyboard,
+ quit_shortcuts: _, // not shown in ui
tessellation_options,
repaint_on_widget_change,
max_passes,
@@ -564,11 +579,13 @@ impl Focus {
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::Focus,
- target,
+ target_node,
+ target_tree,
data: None,
}) = event
+ && *target_tree == accesskit::TreeId::ROOT
{
- self.id_requested_by_accesskit = Some(*target);
+ self.id_requested_by_accesskit = Some(*target_node);
}
}
}
diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs
index 495d5dcdf..a0dd6bd91 100644
--- a/crates/egui/src/response.rs
+++ b/crates/egui/src/response.rs
@@ -55,7 +55,7 @@ pub struct Response {
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
#[doc(hidden)]
- pub interact_pointer_pos: Option,
+ pub interact_pointer_pos_or_nan: Pos2,
/// The intrinsic / desired size of the widget.
///
@@ -67,12 +67,22 @@ pub struct Response {
/// At the time of writing, this is only used by external crates
/// for improved layouting.
/// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
- pub intrinsic_size: Option,
+ #[doc(hidden)]
+ pub intrinsic_size_or_nan: Vec2,
#[doc(hidden)]
pub flags: Flags,
}
+#[test]
+fn test_response_size() {
+ assert_eq!(
+ std::mem::size_of::(),
+ 88,
+ "Keep Response small, because we create them often, and we want to keep it lean and fast"
+ );
+}
+
/// A bit set for various boolean properties of `Response`.
#[doc(hidden)]
#[derive(Copy, Clone, Debug)]
@@ -141,6 +151,22 @@ bitflags::bitflags! {
}
impl Response {
+ /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
+ ///
+ /// Looks up the [`WidgetRect`] from the current (or previous) pass.
+ pub fn parent_id(&self) -> Id {
+ let id = self.ctx.viewport(|viewport| {
+ viewport
+ .this_pass
+ .widgets
+ .get(self.id)
+ .or_else(|| viewport.prev_pass.widgets.get(self.id))
+ .map(|w| w.parent_id)
+ });
+ debug_assert!(id.is_some(), "WidgetRect for Response not found!");
+ id.unwrap_or(Id::NULL)
+ }
+
/// Returns true if this widget was clicked this frame by the primary button.
///
/// A click is registered when the mouse or touch is released within
@@ -489,7 +515,26 @@ impl Response {
/// `None` if the widget is not being interacted with.
#[inline]
pub fn interact_pointer_pos(&self) -> Option {
- self.interact_pointer_pos
+ let pos = self.interact_pointer_pos_or_nan;
+ if pos.any_nan() { None } else { Some(pos) }
+ }
+
+ /// The intrinsic / desired size of the widget.
+ ///
+ /// This is the size that a non-wrapped, non-truncated, non-justified version of the widget
+ /// would have.
+ ///
+ /// If this is `None`, use [`Self::rect`] instead.
+ #[inline]
+ pub fn intrinsic_size(&self) -> Option {
+ let size = self.intrinsic_size_or_nan;
+ if size.any_nan() { None } else { Some(size) }
+ }
+
+ /// Set the intrinsic / desired size of the widget.
+ #[inline]
+ pub fn set_intrinsic_size(&mut self, size: Vec2) {
+ self.intrinsic_size_or_nan = size;
}
/// If it is a good idea to show a tooltip, where is pointer?
@@ -732,6 +777,7 @@ impl Response {
WidgetRect {
layer_id: self.layer_id,
id: self.id,
+ parent_id: self.parent_id(),
rect: self.rect,
interact_rect: self.interact_rect,
sense: self.sense | sense,
@@ -1007,8 +1053,10 @@ impl Response {
interact_rect: self.interact_rect.union(other.interact_rect),
sense: self.sense.union(other.sense),
flags: self.flags | other.flags,
- interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
- intrinsic_size: None,
+ interact_pointer_pos_or_nan: self
+ .interact_pointer_pos()
+ .unwrap_or(other.interact_pointer_pos_or_nan),
+ intrinsic_size_or_nan: Vec2::NAN,
}
}
}
diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs
index 27ce2516c..8c63b7e75 100644
--- a/crates/egui/src/style.rs
+++ b/crates/egui/src/style.rs
@@ -1,7 +1,11 @@
//! egui theme (spacing, colors, etc).
use emath::Align;
-use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions, text::FontTweak};
+use epaint::{
+ AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions,
+ mutex::Mutex,
+ text::{FontTweak, Tag},
+};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{
@@ -1319,6 +1323,10 @@ pub struct DebugOptions {
/// Show interesting widgets under the mouse cursor.
pub show_widget_hits: bool,
+ /// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the
+ /// previous frame.
+ pub warn_if_rect_changes_id: bool,
+
/// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`].
///
/// See [`emath::GuiRounding`] for more.
@@ -1345,6 +1353,7 @@ impl Default for DebugOptions {
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
+ warn_if_rect_changes_id: cfg!(debug_assertions),
show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
}
@@ -2526,6 +2535,7 @@ impl DebugOptions {
show_resize,
show_interactive_widgets,
show_widget_hits,
+ warn_if_rect_changes_id,
show_unaligned,
show_focused_widget,
} = self;
@@ -2557,6 +2567,11 @@ impl DebugOptions {
ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
+ ui.checkbox(
+ warn_if_rect_changes_id,
+ "Warn if a Rect changes Id between frames",
+ );
+
ui.checkbox(
show_unaligned,
"Show rectangles not aligned to integer point coordinates",
@@ -2876,7 +2891,7 @@ impl Widget for &mut crate::Frame {
impl Widget for &mut FontTweak {
fn ui(self, ui: &mut Ui) -> Response {
- let original: FontTweak = *self;
+ let original: FontTweak = self.clone();
let mut response = Grid::new("font_tweak")
.num_columns(2)
@@ -2886,6 +2901,7 @@ impl Widget for &mut FontTweak {
y_offset_factor,
y_offset,
hinting_override,
+ coords,
} = self;
ui.label("Scale");
@@ -2913,6 +2929,50 @@ impl Widget for &mut FontTweak {
ui.selectable_value(hinting_override, Some(true), "Enable");
ui.selectable_value(hinting_override, Some(false), "Disable");
});
+ ui.end_row();
+
+ ui.label("coords");
+ ui.end_row();
+ let mut to_remove = None;
+ for (i, (tag, value)) in coords.as_mut().iter_mut().enumerate() {
+ let tag_text = ui.ctx().data_mut(|data| {
+ let tag = *tag;
+ Arc::clone(data.get_temp_mut_or_insert_with(ui.id().with(i), move || {
+ Arc::new(Mutex::new(tag.to_string()))
+ }))
+ });
+
+ let tag_text = &mut *tag_text.lock();
+ let response = ui.text_edit_singleline(tag_text);
+ if response.changed()
+ && let Ok(new_tag) = Tag::new_checked(tag_text.as_bytes())
+ {
+ *tag = new_tag;
+ }
+ // Reset stale text when not actively editing
+ // (e.g. after an item was removed and indices shifted)
+ if !response.has_focus()
+ && Tag::new_checked(tag_text.as_bytes()).ok() != Some(*tag)
+ {
+ *tag_text = tag.to_string();
+ }
+
+ ui.add(DragValue::new(value));
+ if ui.small_button("🗑").clicked() {
+ to_remove = Some(i);
+ }
+ ui.end_row();
+ }
+ if let Some(i) = to_remove {
+ coords.remove(i);
+ }
+ if ui.button("Add coord").clicked() {
+ coords.push(b"wght", 0.0);
+ }
+ if ui.button("Clear coords").clicked() {
+ coords.clear();
+ }
+ ui.end_row();
if ui.button("Reset").clicked() {
*self = Default::default();
diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs
index 974a334d0..650e7e5c0 100644
--- a/crates/egui/src/text_selection/accesskit_text.rs
+++ b/crates/egui/src/text_selection/accesskit_text.rs
@@ -4,6 +4,26 @@ use crate::{Context, Galley, Id};
use super::{CCursorRange, text_cursor_state::is_word_char};
+/// AccessKit's `word_starts` uses `u8` indices, so text runs cannot exceed this length.
+pub(crate) const MAX_CHARS_PER_TEXT_RUN: usize = 255;
+
+/// Convert a (row, column) layout cursor position to a text run node ID and character index,
+/// accounting for rows that are split into multiple text runs.
+fn text_run_position(parent_id: Id, row: usize, column: usize) -> accesskit::TextPosition {
+ // When column lands exactly on a chunk boundary (e.g., 255), it refers to
+ // the end of the previous chunk, not the start of a new one.
+ let chunk_index = if column > 0 && column.is_multiple_of(MAX_CHARS_PER_TEXT_RUN) {
+ column / MAX_CHARS_PER_TEXT_RUN - 1
+ } else {
+ column / MAX_CHARS_PER_TEXT_RUN
+ };
+ let character_index = column - chunk_index * MAX_CHARS_PER_TEXT_RUN;
+ accesskit::TextPosition {
+ node: parent_id.with(row).with(chunk_index).accesskit_id(),
+ character_index,
+ }
+}
+
/// Update accesskit with the current text state.
pub fn update_accesskit_for_text_widget(
ctx: &Context,
@@ -20,14 +40,8 @@ pub fn update_accesskit_for_text_widget(
let anchor = galley.layout_from_cursor(cursor_range.secondary);
let focus = galley.layout_from_cursor(cursor_range.primary);
builder.set_text_selection(accesskit::TextSelection {
- anchor: accesskit::TextPosition {
- node: parent_id.with(anchor.row).accesskit_id(),
- character_index: anchor.column,
- },
- focus: accesskit::TextPosition {
- node: parent_id.with(focus.row).accesskit_id(),
- character_index: focus.column,
- },
+ anchor: text_run_position(parent_id, anchor.row, anchor.column),
+ focus: text_run_position(parent_id, focus.row, focus.column),
});
}
@@ -40,61 +54,144 @@ pub fn update_accesskit_for_text_widget(
return;
};
+ let mut prev_row_ended_with_newline = true;
+
for (row_index, row) in galley.rows.iter().enumerate() {
- let row_id = parent_id.with(row_index);
+ let glyph_count = row.glyphs.len();
+ let mut value = String::with_capacity(glyph_count);
+ let mut character_lengths = Vec::::with_capacity(glyph_count);
+ let mut character_positions = Vec::::with_capacity(glyph_count);
+ let mut character_widths = Vec::::with_capacity(glyph_count);
+ let mut word_starts = Vec::::new();
+ // For soft-wrapped continuation rows, treat the start as a word
+ // boundary so the first word character gets a `word_starts` entry.
+ // Paragraph-starting runs (first row or after a newline) get an
+ // implicit word start from AccessKit, so they don't need this.
+ let mut was_at_word_end = !prev_row_ended_with_newline;
- ctx.register_accesskit_parent(row_id, parent_id);
+ for glyph in &row.glyphs {
+ let is_word_char = is_word_char(glyph.chr);
+ if is_word_char && was_at_word_end {
+ word_starts.push(character_lengths.len());
+ }
+ was_at_word_end = !is_word_char;
+ let old_len = value.len();
+ value.push(glyph.chr);
+ character_lengths.push((value.len() - old_len) as _);
+ character_positions.push(glyph.pos.x - row.pos.x);
+ character_widths.push(glyph.advance_width);
+ }
- ctx.accesskit_node_builder(row_id, |builder| {
- builder.set_role(accesskit::Role::TextRun);
- 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(),
- x1: rect.max.x.into(),
- y1: rect.max.y.into(),
- });
- builder.set_text_direction(accesskit::TextDirection::LeftToRight);
- // TODO(mwcampbell): Set more node fields for the row
- // once AccessKit adapters expose text formatting info.
+ if row.ends_with_newline {
+ value.push('\n');
+ character_lengths.push(1);
+ character_positions.push(row.size.x);
+ character_widths.push(0.0);
+ }
- let glyph_count = row.glyphs.len();
- let mut value = String::new();
- value.reserve(glyph_count);
- let mut character_lengths = Vec::::with_capacity(glyph_count);
- let mut character_positions = Vec::::with_capacity(glyph_count);
- let mut character_widths = Vec::::with_capacity(glyph_count);
- let mut word_lengths = Vec::::new();
- let mut was_at_word_end = false;
- let mut last_word_start = 0usize;
+ let total_chars = character_lengths.len();
- for glyph in &row.glyphs {
- let is_word_char = is_word_char(glyph.chr);
- if is_word_char && was_at_word_end {
- word_lengths.push((character_lengths.len() - last_word_start) as _);
- last_word_start = character_lengths.len();
+ if total_chars <= MAX_CHARS_PER_TEXT_RUN {
+ let run_id = parent_id.with(row_index).with(0usize);
+ ctx.register_accesskit_parent(run_id, parent_id);
+
+ ctx.accesskit_node_builder(run_id, |builder| {
+ builder.set_role(accesskit::Role::TextRun);
+ builder.set_text_direction(accesskit::TextDirection::LeftToRight);
+ // TODO(mwcampbell): Set more node fields for the row
+ // once AccessKit adapters expose text formatting info.
+
+ 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(),
+ x1: rect.max.x.into(),
+ y1: rect.max.y.into(),
+ });
+ builder.set_value(value);
+ builder.set_character_lengths(character_lengths);
+
+ let pos_offset = character_positions.first().copied().unwrap_or(0.0);
+ for p in &mut character_positions {
+ *p -= pos_offset;
}
- was_at_word_end = !is_word_char;
- let old_len = value.len();
- value.push(glyph.chr);
- character_lengths.push((value.len() - old_len) as _);
- character_positions.push(glyph.pos.x - row.pos.x);
- character_widths.push(glyph.advance_width);
- }
+ builder.set_character_positions(character_positions);
+ builder.set_character_widths(character_widths);
- if row.ends_with_newline {
- value.push('\n');
- character_lengths.push(1);
- character_positions.push(row.size.x);
- character_widths.push(0.0);
- }
- word_lengths.push((character_lengths.len() - last_word_start) as _);
+ let chunk_word_starts: Vec = word_starts.iter().map(|&ws| ws as u8).collect();
+ builder.set_word_starts(chunk_word_starts);
+ });
+ } else {
+ let num_chunks = total_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN);
+ let mut byte_offset = 0usize;
- builder.set_value(value);
- builder.set_character_lengths(character_lengths);
- builder.set_character_positions(character_positions);
- builder.set_character_widths(character_widths);
- builder.set_word_lengths(word_lengths);
- });
+ for chunk_idx in 0..num_chunks {
+ let char_start = chunk_idx * MAX_CHARS_PER_TEXT_RUN;
+ let char_end = (char_start + MAX_CHARS_PER_TEXT_RUN).min(total_chars);
+
+ let byte_start = byte_offset;
+ let chunk_byte_len: usize = character_lengths[char_start..char_end]
+ .iter()
+ .map(|&l| l as usize)
+ .sum();
+ let byte_end = byte_start + chunk_byte_len;
+ byte_offset = byte_end;
+
+ let run_id = parent_id.with(row_index).with(chunk_idx);
+ ctx.register_accesskit_parent(run_id, parent_id);
+
+ ctx.accesskit_node_builder(run_id, |builder| {
+ builder.set_role(accesskit::Role::TextRun);
+ builder.set_text_direction(accesskit::TextDirection::LeftToRight);
+ // TODO(mwcampbell): Set more node fields for the row
+ // once AccessKit adapters expose text formatting info.
+
+ if chunk_idx > 0 {
+ let prev_id = parent_id.with(row_index).with(chunk_idx - 1);
+ builder.set_previous_on_line(prev_id.accesskit_id());
+ }
+ if chunk_idx + 1 < num_chunks {
+ let next_id = parent_id.with(row_index).with(chunk_idx + 1);
+ builder.set_next_on_line(next_id.accesskit_id());
+ }
+
+ let row_rect = row.rect_without_leading_space();
+ let chunk_x0 = row.pos.x + character_positions[char_start];
+ let chunk_x1 = row.pos.x
+ + character_positions[char_end - 1]
+ + character_widths[char_end - 1];
+ let chunk_rect = emath::Rect::from_min_max(
+ emath::pos2(chunk_x0, row_rect.min.y),
+ emath::pos2(chunk_x1, row_rect.max.y),
+ );
+ let rect = global_from_galley * chunk_rect;
+ builder.set_bounds(accesskit::Rect {
+ x0: rect.min.x.into(),
+ y0: rect.min.y.into(),
+ x1: rect.max.x.into(),
+ y1: rect.max.y.into(),
+ });
+ builder.set_value(value[byte_start..byte_end].to_owned());
+ builder.set_character_lengths(character_lengths[char_start..char_end].to_vec());
+
+ let pos_offset = character_positions[char_start];
+ let chunk_positions: Vec = character_positions[char_start..char_end]
+ .iter()
+ .map(|&p| p - pos_offset)
+ .collect();
+ builder.set_character_positions(chunk_positions);
+ builder.set_character_widths(character_widths[char_start..char_end].to_vec());
+
+ let chunk_word_starts: Vec = word_starts
+ .iter()
+ .filter(|&&ws| ws >= char_start && ws < char_end)
+ .map(|&ws| (ws - char_start) as u8)
+ .collect();
+ builder.set_word_starts(chunk_word_starts);
+ });
+ }
+ }
+
+ prev_row_ended_with_newline = row.ends_with_newline;
}
}
diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs
index a816f5f26..f0445217f 100644
--- a/crates/egui/src/text_selection/cursor_range.rs
+++ b/crates/egui/src/text_selection/cursor_range.rs
@@ -192,10 +192,13 @@ impl CCursorRange {
Event::AccessKitActionRequest(accesskit::ActionRequest {
action: accesskit::Action::SetTextSelection,
- target,
+ target_node,
+ target_tree,
data: Some(accesskit::ActionData::SetTextSelection(selection)),
}) => {
- if _widget_id.accesskit_id() == *target {
+ if _widget_id.accesskit_id() == *target_node
+ && *target_tree == accesskit::TreeId::ROOT
+ {
let primary =
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus);
let secondary =
@@ -224,18 +227,31 @@ fn ccursor_from_accesskit_text_position(
galley: &Galley,
position: &accesskit::TextPosition,
) -> Option {
+ use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN;
+
let mut total_length = 0usize;
for (i, row) in galley.rows.iter().enumerate() {
- let row_id = id.with(i);
- if row_id.accesskit_id() == position.node {
- return Some(CCursor {
- index: total_length + position.character_index,
- prefer_next_row: !(position.character_index == row.glyphs.len()
- && !row.ends_with_newline
- && (i + 1) < galley.rows.len()),
- });
+ let row_chars = row.glyphs.len() + (row.ends_with_newline as usize);
+ let num_chunks = if row_chars == 0 {
+ 1
+ } else {
+ row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN)
+ };
+
+ for chunk_idx in 0..num_chunks {
+ let run_id = id.with(i).with(chunk_idx);
+ if run_id.accesskit_id() == position.node {
+ let column = chunk_idx * MAX_CHARS_PER_TEXT_RUN + position.character_index;
+ return Some(CCursor {
+ index: total_length + column,
+ prefer_next_row: !(column == row.glyphs.len()
+ && !row.ends_with_newline
+ && (i + 1) < galley.rows.len()),
+ });
+ }
}
- total_length += row.glyphs.len() + (row.ends_with_newline as usize);
+
+ total_length += row_chars;
}
None
}
diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs
index fead390fe..e41d7a436 100644
--- a/crates/egui/src/text_selection/visuals.rs
+++ b/crates/egui/src/text_selection/visuals.rs
@@ -67,9 +67,7 @@ pub fn paint_text_selection(
let first_vertex_index = row
.glyphs
.get(first_glyph_index)
- .map_or(row.visuals.glyph_vertex_range.start, |g| {
- g.first_vertex as _
- });
+ .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
let last_vertex_index = row
.glyphs
.get(last_glyph_index)
diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs
index 6a1fb7ba4..d2888b73e 100644
--- a/crates/egui/src/ui.rs
+++ b/crates/egui/src/ui.rs
@@ -173,6 +173,7 @@ impl Ui {
ui.ctx().create_widget(
WidgetRect {
id: ui.unique_id,
+ parent_id: ui.id,
layer_id: ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -339,6 +340,7 @@ impl Ui {
child_ui.ctx().create_widget(
WidgetRect {
id: child_ui.unique_id,
+ parent_id: self.id,
layer_id: child_ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -1043,6 +1045,7 @@ impl Ui {
self.ctx().create_widget(
WidgetRect {
id,
+ parent_id: self.id,
layer_id: self.layer_id(),
rect,
interact_rect: self.clip_rect().intersect(rect),
@@ -1112,6 +1115,7 @@ impl Ui {
let mut response = self.ctx().create_widget(
WidgetRect {
id: self.unique_id,
+ parent_id: self.id,
layer_id: self.layer_id(),
rect: self.min_rect(),
interact_rect: self.clip_rect().intersect(self.min_rect()),
@@ -1281,7 +1285,7 @@ impl Ui {
pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
let (id, rect) = self.allocate_space(desired_size);
let mut response = self.interact(rect, id, sense);
- response.intrinsic_size = Some(desired_size);
+ response.set_intrinsic_size(desired_size);
response
}
diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs
index 686fdcb47..87786a726 100644
--- a/crates/egui/src/ui_builder.rs
+++ b/crates/egui/src/ui_builder.rs
@@ -54,8 +54,8 @@ impl UiBuilder {
///
/// This is a shortcut for `.id_salt(my_id).global_scope(true)`.
#[inline]
- pub fn id(mut self, id: impl Hash) -> Self {
- self.id_salt = Some(Id::new(id));
+ pub fn id(mut self, id: Id) -> Self {
+ self.id_salt = Some(id);
self.global_scope = true;
self
}
diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs
index a84dde519..b6fc9f7bb 100644
--- a/crates/egui/src/widget_rect.rs
+++ b/crates/egui/src/widget_rect.rs
@@ -17,6 +17,12 @@ pub struct WidgetRect {
/// You can ensure globally unique ids using [`crate::Ui::push_id`].
pub id: Id,
+ /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
+ ///
+ /// Used by debug checks to distinguish true id-instability from
+ /// cascading id shifts caused by a parent Ui's auto-id changing.
+ pub parent_id: Id,
+
/// What layer the widget is on.
pub layer_id: LayerId,
@@ -46,6 +52,7 @@ impl WidgetRect {
pub fn transform(self, transform: emath::TSTransform) -> Self {
let Self {
id,
+ parent_id,
layer_id,
rect,
interact_rect,
@@ -54,6 +61,7 @@ impl WidgetRect {
} = self;
Self {
id,
+ parent_id,
layer_id,
rect: transform * rect,
interact_rect: transform * interact_rect,
diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs
index 5d91f4bc1..8bc88344b 100644
--- a/crates/egui/src/widget_text.rs
+++ b/crates/egui/src/widget_text.rs
@@ -1,5 +1,5 @@
use emath::GuiRounding as _;
-use epaint::text::TextFormat;
+use epaint::text::{IntoTag, TextFormat, VariationCoords};
use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
@@ -34,6 +34,7 @@ pub struct RichText {
background_color: Color32,
expand_bg: f32,
text_color: Option,
+ coords: VariationCoords,
code: bool,
strong: bool,
weak: bool,
@@ -55,6 +56,7 @@ impl Default for RichText {
background_color: Default::default(),
expand_bg: 1.0,
text_color: Default::default(),
+ coords: Default::default(),
code: Default::default(),
strong: Default::default(),
weak: Default::default(),
@@ -196,6 +198,23 @@ impl RichText {
self
}
+ /// Add a variation coordinate.
+ #[inline]
+ pub fn variation(mut self, tag: impl IntoTag, coord: f32) -> Self {
+ self.coords.push(tag, coord);
+ self
+ }
+
+ /// Override the variation coordinates completely.
+ #[inline]
+ pub fn variations(
+ mut self,
+ variations: impl IntoIterator
- ,
+ ) -> Self {
+ self.coords = VariationCoords::new(variations);
+ self
+ }
+
/// Override the [`TextStyle`].
#[inline]
pub fn text_style(mut self, text_style: TextStyle) -> Self {
@@ -391,6 +410,7 @@ impl RichText {
background_color,
expand_bg,
text_color: _, // already used by `get_text_color`
+ coords,
code,
strong: _, // already used by `get_text_color`
weak: _, // already used by `get_text_color`
@@ -449,6 +469,7 @@ impl RichText {
line_height,
color: text_color,
background: background_color,
+ coords,
italics,
underline,
strikethrough,
diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs
index 4b4c2fe32..7d9dddf0d 100644
--- a/crates/egui/src/widgets/button.rs
+++ b/crates/egui/src/widgets/button.rs
@@ -240,6 +240,18 @@ impl<'a> Button<'a> {
self
}
+ /// Show some text on the left side of the button.
+ #[inline]
+ pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
+ self.layout.push_left(Atom::grow());
+
+ for atom in left_text.into_atoms() {
+ self.layout.push_left(atom);
+ }
+
+ self
+ }
+
/// Show some text on the right side of the button.
#[inline]
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
@@ -259,6 +271,13 @@ impl<'a> Button<'a> {
self
}
+ /// Set the gap between atoms.
+ #[inline]
+ pub fn gap(mut self, gap: f32) -> Self {
+ self.layout = self.layout.gap(gap);
+ self
+ }
+
/// Show the button and return a [`AtomLayoutResponse`] for painting custom contents.
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
let Button {
diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs
index 7841fee61..1297b614b 100644
--- a/crates/egui/src/widgets/drag_value.rs
+++ b/crates/egui/src/widgets/drag_value.rs
@@ -1,11 +1,10 @@
-#![expect(clippy::needless_pass_by_value)] // False positives with `impl ToString`
-
-use std::{cmp::Ordering, ops::RangeInclusive};
-
use crate::{
- Button, CursorIcon, Id, Key, MINUS_CHAR_STR, Modifiers, NumExt as _, Response, RichText, Sense,
- TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text,
+ Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR,
+ Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget,
+ WidgetInfo, emath, text,
};
+use emath::Vec2;
+use std::{cmp::Ordering, ops::RangeInclusive};
// ----------------------------------------------------------------------------
@@ -38,8 +37,7 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
pub struct DragValue<'a> {
get_set_value: GetSetValue<'a>,
speed: f64,
- prefix: String,
- suffix: String,
+ atoms: Atoms<'a>,
range: RangeInclusive,
clamp_existing_to_range: bool,
min_decimals: usize,
@@ -50,6 +48,8 @@ pub struct DragValue<'a> {
}
impl<'a> DragValue<'a> {
+ const ATOM_ID: &'static str = "drag_item";
+
pub fn new(value: &'a mut Num) -> Self {
let slf = Self::from_get_set(move |v: Option| {
if let Some(v) = v {
@@ -66,11 +66,12 @@ impl<'a> DragValue<'a> {
}
pub fn from_get_set(get_set_value: impl 'a + FnMut(Option) -> f64) -> Self {
+ let atoms = Atoms::new(Atom::custom(Id::new(Self::ATOM_ID), Vec2::ZERO).atom_grow(true));
+
Self {
get_set_value: Box::new(get_set_value),
speed: 1.0,
- prefix: Default::default(),
- suffix: Default::default(),
+ atoms,
range: f64::NEG_INFINITY..=f64::INFINITY,
clamp_existing_to_range: true,
min_decimals: 0,
@@ -164,15 +165,15 @@ impl<'a> DragValue<'a> {
/// Show a prefix before the number, e.g. "x: "
#[inline]
- pub fn prefix(mut self, prefix: impl ToString) -> Self {
- self.prefix = prefix.to_string();
+ pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self {
+ self.atoms.extend_left(prefix.into_atoms());
self
}
/// Add a suffix to the number, this can be e.g. a unit ("°" or " m")
#[inline]
- pub fn suffix(mut self, suffix: impl ToString) -> Self {
- self.suffix = suffix.to_string();
+ pub fn suffix(mut self, suffix: impl IntoAtoms<'a>) -> Self {
+ self.atoms.extend_right(suffix.into_atoms());
self
}
@@ -433,8 +434,7 @@ impl Widget for DragValue<'_> {
speed,
range,
clamp_existing_to_range,
- prefix,
- suffix,
+ mut atoms,
min_decimals,
max_decimals,
custom_formatter,
@@ -442,6 +442,23 @@ impl Widget for DragValue<'_> {
update_while_editing,
} = self;
+ let mut prefix_text = String::new();
+ let mut suffix_text = String::new();
+ let mut past_value = false;
+ let atom_id = Id::new(Self::ATOM_ID);
+ for atom in atoms.iter() {
+ if atom.id == Some(atom_id) {
+ past_value = true;
+ }
+ if let AtomKind::Text(text) = &atom.kind {
+ if past_value {
+ suffix_text.push_str(text.text());
+ } else {
+ prefix_text.push_str(text.text());
+ }
+ }
+ }
+
let shift = ui.input(|i| i.modifiers.shift_only());
// The widget has the same ID whether it's in edit or button mode.
let id = ui.next_auto_id();
@@ -543,8 +560,6 @@ impl Widget for DragValue<'_> {
}
}
- // some clones below are redundant if AccessKit is disabled
- #[expect(clippy::redundant_clone)]
let mut response = if is_kb_editing {
let mut value_text = ui
.data_mut(|data| data.remove_temp::(id))
@@ -586,13 +601,20 @@ impl Widget for DragValue<'_> {
ui.data_mut(|data| data.insert_temp(id, value_text));
response
} else {
- let button = Button::new(
- RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
- .text_style(text_style),
- )
- .wrap_mode(TextWrapMode::Extend)
- .sense(Sense::click_and_drag())
- .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
+ atoms.map_atoms(|atom| {
+ if atom.id == Some(atom_id) {
+ RichText::new(value_text.clone())
+ .text_style(text_style.clone())
+ .into()
+ } else {
+ atom
+ }
+ });
+ let button = Button::new(atoms)
+ .wrap_mode(TextWrapMode::Extend)
+ .sense(Sense::click_and_drag())
+ .gap(0.0)
+ .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
let cursor_icon = if value <= *range.start() {
CursorIcon::ResizeEast
@@ -607,10 +629,8 @@ impl Widget for DragValue<'_> {
if ui.style().explanation_tooltips {
response = response.on_hover_text(format!(
- "{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
- prefix,
+ "{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32
- suffix
));
}
@@ -704,7 +724,7 @@ impl Widget for DragValue<'_> {
// The value is exposed as a string by the text edit widget
// when in edit mode.
if !is_kb_editing {
- let value_text = format!("{prefix}{value_text}{suffix}");
+ let value_text = format!("{prefix_text}{value_text}{suffix_text}");
builder.set_value(value_text);
}
});
diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs
index 284cfd12c..7b2d3a3ba 100644
--- a/crates/egui/src/widgets/label.rs
+++ b/crates/egui/src/widgets/label.rs
@@ -220,7 +220,7 @@ impl Label {
.rect_without_leading_space()
.translate(pos.to_vec2());
let mut response = ui.allocate_rect(rect, sense);
- response.intrinsic_size = Some(galley.intrinsic_size());
+ response.set_intrinsic_size(galley.intrinsic_size());
for placed_row in galley.rows.iter().skip(1) {
let rect = placed_row.rect().translate(pos.to_vec2());
response |= ui.allocate_rect(rect, sense);
@@ -256,7 +256,7 @@ impl Label {
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
- response.intrinsic_size = Some(galley.intrinsic_size());
+ response.set_intrinsic_size(galley.intrinsic_size());
let galley_pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),
diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs
index fbf25babf..ef668a02e 100644
--- a/crates/egui/src/widgets/text_edit/builder.rs
+++ b/crates/egui/src/widgets/text_edit/builder.rs
@@ -1,15 +1,13 @@
use std::sync::Arc;
use emath::{Rect, TSTransform};
-use epaint::{
- StrokeKind,
- text::{Galley, LayoutJob, cursor::CCursor},
-};
+use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
- Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
- Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
- TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
+ Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
+ Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
+ KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
+ TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
@@ -67,15 +65,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc {
text: &'t mut dyn TextBuffer,
- hint_text: WidgetText,
- hint_text_font: Option,
+ prefix: Atoms<'static>,
+ suffix: Atoms<'static>,
+ hint_text: Atoms<'static>,
id: Option,
id_salt: Option,
font_selection: FontSelection,
text_color: Option,
layouter: Option>,
password: bool,
- frame: bool,
+ frame: Option,
margin: Margin,
multiline: bool,
interactive: bool,
@@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> {
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
Self {
text,
+ prefix: Default::default(),
+ suffix: Default::default(),
hint_text: Default::default(),
- hint_text_font: None,
id: None,
id_salt: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
- frame: true,
+ frame: None,
margin: Margin::symmetric(4, 2),
multiline: true,
interactive: true,
@@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> {
/// # });
/// ```
#[inline]
- pub fn hint_text(mut self, hint_text: impl Into) -> Self {
- self.hint_text = hint_text.into();
+ pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
+ self.hint_text = hint_text.into_atoms();
+ self
+ }
+
+ /// Add a prefix to the text edit. This will always be shown before the editable text.
+ #[inline]
+ pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
+ self.prefix = prefix.into_atoms();
+ self
+ }
+
+ /// Add a suffix to the text edit. This will always be shown after the editable text.
+ #[inline]
+ pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
+ self.suffix = suffix.into_atoms();
self
}
@@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> {
self
}
- /// Set a specific style for the hint text.
- #[inline]
- pub fn hint_text_font(mut self, hint_text_font: impl Into) -> Self {
- self.hint_text_font = Some(hint_text_font.into());
- self
- }
-
/// If true, hide the letters from view and prevent copying from the field.
#[inline]
pub fn password(mut self, password: bool) -> Self {
@@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> {
self
}
- /// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
+ /// Customize the [`Frame`] around the text edit.
#[inline]
- pub fn frame(mut self, frame: bool) -> Self {
- self.frame = frame;
+ pub fn frame(mut self, frame: Frame) -> Self {
+ self.frame = Some(frame);
self
}
@@ -402,7 +409,7 @@ impl<'t> TextEdit<'t> {
impl Widget for TextEdit<'_> {
fn ui(self, ui: &mut Ui) -> Response {
- self.show(ui).response
+ self.show(ui).response.response
}
}
@@ -423,63 +430,18 @@ impl TextEdit<'_> {
/// # });
/// ```
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
- let is_mutable = self.text.is_mutable();
- let frame = self.frame;
- let where_to_put_background = ui.painter().add(Shape::Noop);
- let background_color = self
- .background_color
- .unwrap_or_else(|| ui.visuals().text_edit_bg_color());
- let output = self.show_content(ui);
-
- if frame {
- let visuals = ui.style().interact(&output.response);
- let frame_rect = output.response.rect.expand(visuals.expansion);
- let shape = if is_mutable {
- if output.response.has_focus() {
- epaint::RectShape::new(
- frame_rect,
- visuals.corner_radius,
- background_color,
- ui.visuals().selection.stroke,
- StrokeKind::Inside,
- )
- } else {
- epaint::RectShape::new(
- frame_rect,
- visuals.corner_radius,
- background_color,
- visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
- StrokeKind::Inside,
- )
- }
- } else {
- let visuals = &ui.style().visuals.widgets.inactive;
- epaint::RectShape::stroke(
- frame_rect,
- visuals.corner_radius,
- visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
- StrokeKind::Inside,
- )
- };
-
- ui.painter().set(where_to_put_background, shape);
- }
-
- output
- }
-
- fn show_content(self, ui: &mut Ui) -> TextEditOutput {
let TextEdit {
text,
- hint_text,
- hint_text_font,
+ prefix,
+ suffix,
+ mut hint_text,
id,
id_salt,
font_selection,
text_color,
layouter,
password,
- frame: _,
+ frame,
margin,
multiline,
interactive,
@@ -492,7 +454,7 @@ impl TextEdit<'_> {
clip_text,
char_limit,
return_key,
- background_color: _,
+ background_color,
} = self;
let text_color = text_color
@@ -501,18 +463,16 @@ impl TextEdit<'_> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_str().to_owned();
- let hint_text_str = hint_text.text().to_owned();
+ let hint_text_str = hint_text.text().unwrap_or_default().to_string();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
- let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
- let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
- let wrap_width = if ui.layout().horizontal_justify() {
- available_width
- } else {
- desired_width.min(available_width)
- };
+ let available_width = ui.available_width().at_least(MIN_WIDTH);
+ let desired_width = desired_width
+ .unwrap_or_else(|| ui.spacing().text_edit_width)
+ .at_least(min_size.x);
+ let allocate_width = desired_width.at_most(available_width);
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
@@ -527,27 +487,18 @@ impl TextEdit<'_> {
let layouter = layouter.unwrap_or(&mut default_layouter);
- let mut galley = layouter(ui, text, wrap_width);
-
- let desired_inner_width = if clip_text {
- wrap_width // visual clipping with scroll in singleline input.
- } else {
- galley.size().x.max(wrap_width)
- };
- let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
- let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height));
- let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
- let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
- let rect = outer_rect - margin; // inner rect (excluding frame/margin).
+ let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
let id = id.unwrap_or_else(|| {
if let Some(id_salt) = id_salt {
ui.make_persistent_id(id_salt)
} else {
- auto_id // Since we are only storing the cursor a persistent Id is not super important
+ // Since we are only storing the cursor a persistent Id is not super important
+ let id = ui.next_auto_id();
+ ui.skip_ahead_auto_ids(1);
+ id
}
});
- let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
// On touch screens (e.g. mobile in `eframe` web), should
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
@@ -565,12 +516,218 @@ impl TextEdit<'_> {
} else {
Sense::hover()
};
- let mut response = ui.interact(outer_rect, id, sense);
- response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
- // Don't sent `OutputEvent::Clicked` when a user presses the space bar
+ let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
+ let mut cursor_range = None;
+ let mut prev_cursor_range = None;
+
+ let mut text_changed = false;
+ let text_mutable = text.is_mutable();
+
+ let mut handle_events = |ui: &Ui, galley: &mut Arc, layouter, wrap_width, text| {
+ if interactive && ui.memory(|mem| mem.has_focus(id)) {
+ ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
+
+ let default_cursor_range = if cursor_at_end {
+ CCursorRange::one(galley.end())
+ } else {
+ CCursorRange::default()
+ };
+ prev_cursor_range = state.cursor.range(galley);
+
+ let (changed, new_cursor_range) = events(
+ ui,
+ &mut state,
+ text,
+ galley,
+ layouter,
+ id,
+ wrap_width,
+ multiline,
+ password,
+ default_cursor_range,
+ char_limit,
+ event_filter,
+ return_key,
+ );
+
+ if changed {
+ text_changed = true;
+ }
+ cursor_range = Some(new_cursor_range);
+ }
+ };
+
+ // We need to calculate the galley within the atom closure, so we can calculate it based on
+ // the available width (in case of wrapping multiline text edits). But we show it later,
+ // so we can clip it to the available size. Thus, extract it from the atom closure here.
+ let mut get_galley = None;
+ let inner_rect_id = Id::new("text_edit_rect");
+ let mut response = {
+ let any_shrink = hint_text.any_shrink();
+ // Ideally we could just do `let mut atoms = prefix` here, but that won't compile
+ // but due to servo/rust-smallvec#146 (also see the comment below).
+ let mut atoms: Atoms<'_> = Atoms::new(());
+
+ // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
+ // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
+ for atom in prefix {
+ atoms.push_right(atom);
+ }
+
+ if text.as_str().is_empty() && !hint_text.is_empty() {
+ // Add hint_text (if any):
+ let mut shrunk = any_shrink;
+ let mut first = true;
+
+ // Since we can't set a fallback color per atom, we have to override it here.
+ // Sucks, since it means users won't be able to override it.
+ hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
+
+ for mut atom in hint_text {
+ if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
+ // elide the hint_text if needed
+ atom = atom.atom_shrink(true);
+ shrunk = true;
+ }
+
+ if first {
+ // The first atom in the hint text gets inner_rect_id, so we can know
+ // where to paint the cursor
+ atom = atom.atom_id(inner_rect_id);
+ first = false;
+ }
+
+ // The hint text should be shown left top instead of centered (important for
+ // multi line text edits)
+ atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
+ }
+
+ // Calculate the empty galley, so it can be read later. The available width is
+ // technically wrong, but doesn't matter since the galley is empty
+ let available_width = allocate_width - margin.sum().x;
+ let galley = layouter(ui, text, available_width);
+
+ // We can't update the galley immediately here, since it would show both hint text
+ // and the newly typed letter. So we pass a clone instead, and accept having a frame
+ // delay on the very first keystroke.
+ let mut galley_clone = Arc::clone(&galley);
+ handle_events(ui, &mut galley_clone, layouter, available_width, text);
+
+ get_galley = Some(galley);
+ } else {
+ // We need a closure here, so we can calculate the galley based on the available
+ // width (after adding suffix and prefix), for correct wrapping in multi line text
+ // edits
+ atoms.push_right(
+ AtomKind::closure(|ui, args| {
+ let mut galley = layouter(ui, text, args.available_size.x);
+
+ // Handling events here allows us to update the galley immediately on
+ // keystrokes, avoiding frame delays, and ensuring the scroll_to within
+ // ScrollAreas works correctly.
+ handle_events(ui, &mut galley, layouter, args.available_size.x, text);
+
+ let intrinsic_size = galley.intrinsic_size();
+ let mut size = galley.size();
+ size.y = size.y.at_least(min_inner_height);
+ if clip_text {
+ size.x = size.x.at_most(args.available_size.x);
+ }
+
+ // We paint the galley later, so we can do clipping and offsetting
+ get_galley = Some(galley);
+ IntoSizedResult {
+ intrinsic_size,
+ sized: SizedAtomKind::Empty { size: Some(size) },
+ }
+ })
+ .atom_id(inner_rect_id)
+ .atom_shrink(clip_text),
+ );
+ }
+
+ // Ensure the suffix is always right-aligned
+ if !suffix.is_empty() {
+ atoms.push_right(Atom::grow());
+ }
+
+ // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
+ // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
+ for atom in suffix {
+ atoms.push_right(atom);
+ }
+
+ let custom_frame = frame.is_some();
+ let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
+
+ let min_height = min_inner_height + frame.total_margin().sum().y;
+
+ // This wrap mode only affects the hint_text
+ let wrap_mode = if multiline {
+ TextWrapMode::Wrap
+ } else {
+ TextWrapMode::Truncate
+ };
+
+ let mut allocated = AtomLayout::new(atoms)
+ .id(id)
+ .min_size(Vec2::new(allocate_width, min_height))
+ .max_width(allocate_width)
+ .sense(sense)
+ .frame(frame)
+ .align2(Align2::LEFT_TOP)
+ .wrap_mode(wrap_mode)
+ .allocate(ui);
+
+ allocated.frame = if !custom_frame {
+ let visuals = ui.style().interact(&allocated.response);
+ let background_color =
+ background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
+
+ let (corner_radius, background_color, stroke) = if text_mutable {
+ if allocated.response.has_focus() {
+ (
+ visuals.corner_radius,
+ background_color,
+ ui.visuals().selection.stroke,
+ )
+ } else {
+ (visuals.corner_radius, background_color, visuals.bg_stroke)
+ }
+ } else {
+ let visuals = &ui.style().visuals.widgets.inactive;
+ (
+ visuals.corner_radius,
+ Color32::TRANSPARENT,
+ visuals.bg_stroke,
+ )
+ };
+ allocated
+ .frame
+ .fill(background_color)
+ .corner_radius(corner_radius)
+ .inner_margin(
+ allocated.frame.inner_margin
+ + Margin::same((visuals.expansion - stroke.width).round() as i8),
+ )
+ .outer_margin(Margin::same(-(visuals.expansion as i8)))
+ .stroke(stroke)
+ } else {
+ allocated.frame
+ };
+
+ allocated.paint(ui)
+ };
+
+ let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
+
+ // Our atom closure was now called, so the galley should always be available here
+ let mut galley = get_galley.expect("Galley should be available here");
+
+ // Don't send `OutputEvent::Clicked` when a user presses the space bar
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
- let text_clip_rect = rect;
+ let text_clip_rect = inner_rect;
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
@@ -581,19 +738,19 @@ impl TextEdit<'_> {
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
let cursor_at_pointer =
- galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset);
+ galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset);
if ui.visuals().text_cursor.preview
&& response.hovered()
&& ui.input(|i| i.pointer.is_moving())
{
// text cursor preview:
- let cursor_rect = TSTransform::from_translation(rect.min.to_vec2())
+ let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2())
* cursor_rect(&galley, &cursor_at_pointer, row_height);
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
}
- let is_being_dragged = ui.ctx().is_being_dragged(response.id);
+ let is_being_dragged = ui.is_being_dragged(response.id);
let did_interact = state.cursor.pointer_interaction(
ui,
&response,
@@ -613,44 +770,15 @@ impl TextEdit<'_> {
ui.set_cursor_icon(CursorIcon::Text);
}
- let mut cursor_range = None;
- let prev_cursor_range = state.cursor.range(&galley);
- if interactive && ui.memory(|mem| mem.has_focus(id)) {
- ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
-
- let default_cursor_range = if cursor_at_end {
- CCursorRange::one(galley.end())
- } else {
- CCursorRange::default()
- };
-
- let (changed, new_cursor_range) = events(
- ui,
- &mut state,
- text,
- &mut galley,
- layouter,
- id,
- wrap_width,
- multiline,
- password,
- default_cursor_range,
- char_limit,
- event_filter,
- return_key,
- );
-
- if changed {
- response.mark_changed();
- }
- cursor_range = Some(new_cursor_range);
+ if text_changed {
+ response.mark_changed();
}
let mut galley_pos = align
- .align_size_within_rect(galley.size(), rect)
- .intersect(rect) // limit pos to the response rect area
+ .align_size_within_rect(galley.size(), inner_rect)
+ .intersect(inner_rect) // limit pos to the response rect area
.min;
- let align_offset = rect.left_top() - galley_pos;
+ let align_offset = inner_rect.left_top() - galley_pos;
// Visual clipping for singleline text editor with text larger than width
if clip_text && align_offset.x == 0.0 {
@@ -660,18 +788,18 @@ impl TextEdit<'_> {
};
let mut offset_x = state.text_offset.x;
- let visible_range = offset_x..=offset_x + desired_inner_size.x;
+ let visible_range = offset_x..=offset_x + inner_rect.width();
if !visible_range.contains(&cursor_pos) {
if cursor_pos < *visible_range.start() {
offset_x = cursor_pos;
} else {
- offset_x = cursor_pos - desired_inner_size.x;
+ offset_x = cursor_pos - inner_rect.width();
}
}
offset_x = offset_x
- .at_most(galley.size().x - desired_inner_size.x)
+ .at_most(galley.size().x - inner_rect.width())
.at_least(0.0);
state.text_offset = vec2(offset_x, align_offset.y);
@@ -688,32 +816,7 @@ impl TextEdit<'_> {
false
};
- if ui.is_rect_visible(rect) {
- if text.as_str().is_empty() && !hint_text.is_empty() {
- let hint_text_color = ui.visuals().weak_text_color();
- let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
- let galley = if multiline {
- hint_text.into_galley(
- ui,
- Some(TextWrapMode::Wrap),
- desired_inner_size.x,
- hint_text_font_id,
- )
- } else {
- hint_text.into_galley(
- ui,
- Some(TextWrapMode::Extend),
- f32::INFINITY,
- hint_text_font_id,
- )
- };
- let galley_pos = align
- .align_size_within_rect(galley.size(), rect)
- .intersect(rect)
- .min;
- painter.galley(galley_pos, galley, hint_text_color);
- }
-
+ if ui.is_rect_visible(inner_rect) {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
@@ -721,45 +824,11 @@ impl TextEdit<'_> {
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
- // Allocate additional space if edits were made this frame that changed the size. This is important so that,
- // if there's a ScrollArea, it can properly scroll to the cursor.
- // Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640)
- if !clip_text
- && let extra_size = galley.size() - rect.size()
- && (extra_size.x > 0.0 || extra_size.y > 0.0)
- {
- match ui.layout().main_dir() {
- crate::Direction::LeftToRight | crate::Direction::TopDown => {
- ui.allocate_rect(
- Rect::from_min_size(outer_rect.max, extra_size),
- Sense::hover(),
- );
- }
- crate::Direction::RightToLeft => {
- ui.allocate_rect(
- Rect::from_min_size(
- emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y),
- extra_size,
- ),
- Sense::hover(),
- );
- }
- crate::Direction::BottomUp => {
- ui.allocate_rect(
- Rect::from_min_size(
- emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y),
- extra_size,
- ),
- Sense::hover(),
- );
- }
- }
- } else {
- // Avoid an ID shift during this pass if the textedit grow
- ui.skip_ahead_auto_ids(1);
- }
-
- painter.galley(galley_pos, Arc::clone(&galley), text_color);
+ painter.galley(
+ galley_pos - vec2(galley.rect.left(), 0.0),
+ Arc::clone(&galley),
+ text_color,
+ );
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
@@ -767,7 +836,7 @@ impl TextEdit<'_> {
if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
- ui.scroll_to_rect(primary_cursor_rect + margin, None);
+ ui.scroll_to_rect(primary_cursor_rect, None);
}
if text.is_mutable() && interactive {
@@ -796,9 +865,9 @@ impl TextEdit<'_> {
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
- ui.ctx().output_mut(|o| {
+ ui.output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
- rect: to_global * rect,
+ rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
});
});
@@ -846,24 +915,22 @@ impl TextEdit<'_> {
});
}
- {
- let role = if password {
- accesskit::Role::PasswordInput
- } else if multiline {
- accesskit::Role::MultilineTextInput
- } else {
- accesskit::Role::TextInput
- };
+ let role = if password {
+ accesskit::Role::PasswordInput
+ } else if multiline {
+ accesskit::Role::MultilineTextInput
+ } else {
+ accesskit::Role::TextInput
+ };
- crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
- ui.ctx(),
- id,
- cursor_range,
- role,
- TSTransform::from_translation(galley_pos.to_vec2()),
- &galley,
- );
- }
+ crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
+ ui.ctx(),
+ id,
+ cursor_range,
+ role,
+ TSTransform::from_translation(galley_pos.to_vec2()),
+ &galley,
+ );
TextEditOutput {
response,
@@ -911,7 +978,7 @@ fn events(
event_filter: EventFilter,
return_key: Option,
) -> (bool, CCursorRange) {
- let os = ui.ctx().os();
+ let os = ui.os();
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
@@ -930,13 +997,7 @@ fn events(
let mut any_change = false;
- let mut events = ui.input(|i| i.filtered_events(&event_filter));
-
- if state.ime_enabled {
- remove_ime_incompatible_events(&mut events);
- // Process IME events first:
- events.sort_by_key(|e| !matches!(e, Event::Ime(_)));
- }
+ let events = ui.input(|i| i.filtered_events(&event_filter));
for event in &events {
let did_mutate_text = match event {
@@ -1066,26 +1127,36 @@ fn events(
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
Event::Ime(ime_event) => {
- /// Empty prediction can be produced with [`ImeEvent::Preedit`]
- /// or [`ImeEvent::Commit`] when user press backspace or escape
- /// during IME, so this function should be called in both cases
- /// to clear current text.
+ /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
+ /// might be emitted from different integrations to signify that
+ /// the current IME composition should be cleared.
///
- /// Example platforms where only `ImeEvent::Preedit("")` of
- /// those two events is emitted when the last character in the
- /// prediction is deleted:
- /// - macOS 15.7.3.
- /// - Debian13 with gnome48 and wayland.
+ /// Example integrations where only `ImeEvent::Preedit("")` of
+ /// those two events is emitted when the last character is
+ /// deleted with a backspace:
+ /// - `egui-winit` on macOS 15.7.3.
+ /// - `egui-winit` on Debian13 with gnome48 and wayland.
///
- /// An example platform where only `ImeEvent::Commit("")` of
- /// those two events is emitted when the last character in the
- /// prediction is deleted:
- /// - Safari 26.2 (on macOS 15.7.3).
- fn clear_prediction(
+ /// An example integration where only `ImeEvent::Commit("")` of
+ /// those two events is emitted when the last character is
+ /// deleted with a backspace:
+ /// - `eframe`'s web integration on Safari 26.2 (on macOS
+ /// 15.7.3).
+ ///
+ /// ## Note
+ ///
+ /// The term “pre-edit string” is used by X11 and Wayland, and
+ /// we use “pre-edit text” and “pre-edit range” here in the
+ /// same manner.
+ /// See:
+ ///
+ /// We previously referred to “pre-edit text” as “prediction”,
+ /// which is not standard and can mean different things.
+ fn clear_preedit_text(
text: &mut dyn TextBuffer,
- cursor_range: &CCursorRange,
+ preedit_range: &CCursorRange,
) -> CCursor {
- text.delete_selected(cursor_range)
+ text.delete_selected(preedit_range)
}
match ime_event {
@@ -1094,33 +1165,33 @@ fn events(
state.ime_cursor_range = cursor_range;
None
}
- ImeEvent::Preedit(text_mark) => {
- if text_mark == "\n" || text_mark == "\r" {
+ ImeEvent::Preedit(preedit_text) => {
+ if preedit_text == "\n" || preedit_text == "\r" {
None
} else {
- let mut ccursor = clear_prediction(text, &cursor_range);
+ let mut ccursor = clear_preedit_text(text, &cursor_range);
let start_cursor = ccursor;
- if !text_mark.is_empty() {
- text.insert_text_at(&mut ccursor, text_mark, char_limit);
+ if !preedit_text.is_empty() {
+ text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor))
}
}
- ImeEvent::Commit(prediction) => {
- if prediction == "\n" || prediction == "\r" {
+ ImeEvent::Commit(commit_text) => {
+ if commit_text == "\n" || commit_text == "\r" {
None
} else {
state.ime_enabled = false;
- let mut ccursor = clear_prediction(text, &cursor_range);
+ let mut ccursor = clear_preedit_text(text, &cursor_range);
- if !prediction.is_empty()
+ if !commit_text.is_empty()
&& cursor_range.secondary.index
== state.ime_cursor_range.secondary.index
{
- text.insert_text_at(&mut ccursor, prediction, char_limit);
+ text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
Some(CCursorRange::one(ccursor))
@@ -1159,27 +1230,6 @@ fn events(
// ----------------------------------------------------------------------------
-fn remove_ime_incompatible_events(events: &mut Vec) {
- // Remove key events which cause problems while 'IME' is being used.
- // See https://github.com/emilk/egui/pull/4509
- events.retain(|event| {
- !matches!(
- event,
- Event::Key { repeat: true, .. }
- | Event::Key {
- key: Key::Backspace
- | Key::ArrowUp
- | Key::ArrowDown
- | Key::ArrowLeft
- | Key::ArrowRight,
- ..
- }
- )
- });
-}
-
-// ----------------------------------------------------------------------------
-
/// Returns `Some(new_cursor)` if we did mutate `text`.
fn check_for_mutating_key_press(
os: OperatingSystem,
diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs
index 8149bbe58..3339f325e 100644
--- a/crates/egui/src/widgets/text_edit/output.rs
+++ b/crates/egui/src/widgets/text_edit/output.rs
@@ -5,7 +5,7 @@ use crate::text::CCursorRange;
/// The output from a [`TextEdit`](crate::TextEdit).
pub struct TextEditOutput {
/// The interaction response.
- pub response: crate::Response,
+ pub response: crate::AtomLayoutResponse,
/// How the text was displayed.
pub galley: Arc,
diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml
index 7cde46383..49609746f 100644
--- a/crates/egui_demo_app/Cargo.toml
+++ b/crates/egui_demo_app/Cargo.toml
@@ -25,10 +25,10 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["wgpu", "persistence"]
-# image_viewer adds about 0.9 MB of WASM
web_app = ["http", "persistence"]
accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"]
+easymark = [] # easymark is off by default, because it a pretty shitty markup language
http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"]
image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"]
@@ -42,15 +42,15 @@ wayland = ["eframe/wayland"]
x11 = ["eframe/x11"]
[dependencies]
-chrono = { workspace = true, features = ["js-sys", "wasmbind"] }
eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] }
egui = { workspace = true, features = ["callstack", "default"] }
-egui_demo_lib = { workspace = true, features = ["default", "chrono"] }
+egui_demo_lib = { workspace = true, features = ["default", "jiff"] }
egui_extras = { workspace = true, features = ["default", "image"] }
image = { workspace = true, default-features = false, features = [
# Ensure we can display the test images
"png",
] }
+jiff = { workspace = true, features = ["std", "tz-system", "js"] }
log.workspace = true
profiling.workspace = true
diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs
index 138c05ede..db34c85ac 100644
--- a/crates/egui_demo_app/src/accessibility_inspector.rs
+++ b/crates/egui_demo_app/src/accessibility_inspector.rs
@@ -1,7 +1,7 @@
use std::mem;
-use accesskit::{Action, ActionRequest, NodeId};
-use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler};
+use accesskit::{Action, ActionRequest};
+use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler};
use eframe::epaint::text::TextWrapMode;
use egui::{
@@ -25,7 +25,7 @@ use egui::{
pub struct AccessibilityInspectorPlugin {
pub open: bool,
tree: Option,
- selected_node: Option,
+ selected_node: Option,
queued_action: Option,
}
@@ -113,13 +113,17 @@ impl AccessibilityInspectorPlugin {
Id::new("Accessibility Inspector")
}
- fn selection_ui(&mut self, ui: &mut Ui, selected_node: Id) {
+ fn selection_ui(&mut self, ui: &mut Ui, selected_node: NodeId) {
ui.separator();
if let Some(tree) = &self.tree
- && let Some(node) = tree.state().node_by_id(NodeId::from(selected_node.value()))
+ && let Some(node) = tree.state().node_by_id(selected_node)
{
- let node_response = ui.ctx().read_response(selected_node);
+ // Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
+ #[expect(unsafe_code)]
+ let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) };
+
+ let node_response = ui.ctx().read_response(egui_node_id);
if let Some(widget_response) = node_response {
ui.debug_painter().debug_rect(
@@ -174,8 +178,10 @@ impl AccessibilityInspectorPlugin {
if node.supports_action(action, &|_node| FilterResult::Include)
&& ui.button(format!("{action:?}")).clicked()
{
+ let (target_node, target_tree) = node.locate();
let action_request = ActionRequest {
- target: node.id(),
+ target_node,
+ target_tree,
action,
data: None,
};
@@ -188,8 +194,8 @@ impl AccessibilityInspectorPlugin {
}
}
- fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) {
- if node.id() == Self::id().value().into()
+ fn node_ui(ui: &mut Ui, node: &Node<'_>, selected_node: &mut Option) {
+ if node.locate() == (Self::id().value().into(), accesskit::TreeId::ROOT)
|| node
.value()
.as_deref()
@@ -200,12 +206,12 @@ impl AccessibilityInspectorPlugin {
let label = node
.label()
.or_else(|| node.value())
- .unwrap_or_else(|| node.id().0.to_string());
+ .unwrap_or_else(|| node.locate().0.0.to_string());
let label = format!("({:?}) {}", node.role(), label);
// Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
#[expect(unsafe_code)]
- let egui_node_id = unsafe { Id::from_high_entropy_bits(node.id().0) };
+ let egui_node_id = unsafe { Id::from_high_entropy_bits(node.locate().0.0) };
ui.push_id(node.id(), |ui| {
let child_count = node.children().len();
@@ -228,7 +234,7 @@ impl AccessibilityInspectorPlugin {
collapsing.set_open(!collapsing.is_open());
}
let label_response =
- ui.selectable_value(selected_node, Some(egui_node_id), label.clone());
+ ui.selectable_value(selected_node, Some(node.id()), label.clone());
if label_response.hovered() {
let widget_response = ui.ctx().read_response(egui_node_id);
diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs
index c88f7638c..d83f000a4 100644
--- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs
+++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs
@@ -40,8 +40,8 @@ impl Custom3d {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("custom3d"),
- bind_group_layouts: &[&bind_group_layout],
- push_constant_ranges: &[],
+ bind_group_layouts: &[Some(&bind_group_layout)],
+ immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
@@ -62,7 +62,7 @@ impl Custom3d {
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
- multiview: None,
+ multiview_mask: None,
cache: None,
});
diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs
index d3953320a..dfc4d116b 100644
--- a/crates/egui_demo_app/src/backend_panel.rs
+++ b/crates/egui_demo_app/src/backend_panel.rs
@@ -219,6 +219,10 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
driver,
driver_info,
backend,
+ device_pci_bus_id,
+ subgroup_min_size,
+ subgroup_max_size,
+ transient_saves_memory,
} = &info;
// Example values:
@@ -261,6 +265,19 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.label(format!("0x{device:02X}"));
ui.end_row();
}
+ if !device_pci_bus_id.is_empty() {
+ ui.label("PCI Bus ID:");
+ ui.label(device_pci_bus_id.as_str());
+ ui.end_row();
+ }
+ if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
+ ui.label("Subgroup size:");
+ ui.label(format!("{subgroup_min_size}..={subgroup_max_size}"));
+ ui.end_row();
+ }
+ ui.label("Transient saves memory:");
+ ui.label(format!("{transient_saves_memory}"));
+ ui.end_row();
});
};
diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs
index ea30bda8d..45abccc7f 100644
--- a/crates/egui_demo_app/src/lib.rs
+++ b/crates/egui_demo_app/src/lib.rs
@@ -9,9 +9,10 @@ pub use wrap_app::{Anchor, WrapApp};
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> f64 {
- use chrono::Timelike as _;
- let time = chrono::Local::now().time();
- time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
+ jiff::Zoned::now()
+ .time()
+ .duration_since(jiff::civil::Time::midnight())
+ .as_secs_f64()
}
/// Trait that wraps different parts of the demo app.
diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs
index 5dcc89a3e..313a1a685 100644
--- a/crates/egui_demo_app/src/wrap_app.rs
+++ b/crates/egui_demo_app/src/wrap_app.rs
@@ -8,12 +8,14 @@ use core::any::Any;
use crate::DemoApp;
+#[cfg(feature = "easymark")]
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EasyMarkApp {
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
}
+#[cfg(feature = "easymark")]
impl DemoApp for EasyMarkApp {
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.editor.panels(ui);
@@ -152,12 +154,18 @@ enum Command {
#[cfg_attr(feature = "serde", serde(default))]
pub struct State {
demo: DemoWindows,
+
+ #[cfg(feature = "easymark")]
easy_mark_editor: EasyMarkApp,
+
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
+
#[cfg(feature = "image_viewer")]
image_viewer: crate::apps::ImageViewer,
+
pub clock: FractalClockApp,
+
rendering_test: ColorTestApp,
selected_anchor: Anchor,
@@ -212,6 +220,7 @@ impl WrapApp {
Anchor::Demo,
&mut self.state.demo as &mut dyn DemoApp,
),
+ #[cfg(feature = "easymark")]
(
"🖹 EasyMark editor",
Anchor::EasyMarkEditor,
@@ -400,6 +409,8 @@ impl WrapApp {
}
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cmd: &mut Command) {
+ ui.add_space(8.0);
+
egui::widgets::global_theme_preference_switch(ui);
ui.separator();
diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png
index 1f6b6438f..0c5228a26 100644
--- a/crates/egui_demo_app/tests/snapshots/clock.png
+++ b/crates/egui_demo_app/tests/snapshots/clock.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5878bc5beaf4406c24f23d900aa9ac7c5507e44cb3ade83b743b8b62e7da1615
-size 335355
+oid sha256:63021012cccfca02d09aa424333453140ae4da3ae58fa32b422f6152ba25741c
+size 335394
diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png
index 41ace3480..e14066758 100644
--- a/crates/egui_demo_app/tests/snapshots/custom3d.png
+++ b/crates/egui_demo_app/tests/snapshots/custom3d.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:706ad012e52a8c51175b050b985cca88e2cb306b24f618b7391641397d17cd28
-size 92804
+oid sha256:4470063fe210d2e5170d6609c2603fff1984b8ee76fb65a1f60a1c4cfdf46ce8
+size 92796
diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png
index b11978ce9..a0c1a52de 100644
--- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png
+++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4135662f2b60a10ef8c3b155172d7a3edcf24a625d8286aeaad0614aa8819893
-size 169604
+oid sha256:b9ad01a55950f96a3ae9e48a2c026143d11ffee62bff4f83b4529cd884ce11f0
+size 169683
diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png
index 03aa3077a..ae238b029 100644
--- a/crates/egui_demo_app/tests/snapshots/imageviewer.png
+++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:509020d8885b718900e534c9948cb95ae88e1eee9e113bdfb77a2f75b9a68f7b
-size 96703
+oid sha256:6030f2f3da3dbbdf8bf3eaf429f222acffb624c7696b654d8b6e64273d49be58
+size 99008
diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml
index 3e61187d5..dc57fb092 100644
--- a/crates/egui_demo_lib/Cargo.toml
+++ b/crates/egui_demo_lib/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib"
categories = ["gui", "graphics"]
keywords = ["glow", "egui", "gui", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"]
[lints]
workspace = true
@@ -26,7 +26,7 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
default = []
-chrono = ["egui_extras/datepicker", "dep:chrono"]
+jiff = ["egui_extras/datepicker", "dep:jiff"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["egui/serde", "dep:serde", "egui_extras/serde"]
@@ -42,7 +42,7 @@ egui_extras = { workspace = true, features = ["image", "svg"] }
unicode_names2.workspace = true # this old version has fewer dependencies
#! ### Optional dependencies
-chrono = { workspace = true, optional = true, features = ["js-sys", "wasmbind"] }
+jiff = { workspace = true, optional = true, features = ["std", "js"] }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
diff --git a/crates/egui_demo_lib/data/egui-logo.svg b/crates/egui_demo_lib/data/egui-logo.svg
new file mode 100644
index 000000000..506bb0d80
--- /dev/null
+++ b/crates/egui_demo_lib/data/egui-logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/crates/egui_demo_lib/data/icon.png b/crates/egui_demo_lib/data/icon.png
index 87f15e746..27a6c9534 100644
Binary files a/crates/egui_demo_lib/data/icon.png and b/crates/egui_demo_lib/data/icon.png differ
diff --git a/crates/egui_demo_lib/data/icon.svg b/crates/egui_demo_lib/data/icon.svg
new file mode 100644
index 000000000..1723ec602
--- /dev/null
+++ b/crates/egui_demo_lib/data/icon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/crates/egui_demo_lib/data/peace.svg b/crates/egui_demo_lib/data/peace.svg
deleted file mode 100644
index 4bf3e33a9..000000000
--- a/crates/egui_demo_lib/data/peace.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs
index 56e2f1eaa..853ebf490 100644
--- a/crates/egui_demo_lib/src/demo/about.rs
+++ b/crates/egui_demo_lib/src/demo/about.rs
@@ -27,20 +27,30 @@ impl crate::View for About {
fn ui(&mut self, ui: &mut egui::Ui) {
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
- ui.heading("egui");
+ ui.vertical_centered(|ui| {
+ ui.add_space(4.0);
+ let egui_icon = egui::include_image!("../../data/egui-logo.svg");
+ ui.add(
+ egui::Image::new(egui_icon.clone())
+ .max_height(30.0)
+ .tint(ui.visuals().strong_text_color()),
+ );
+ ui.add_space(4.0);
+ });
+
ui.label(format!(
- "egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \
- On the web it is compiled to WebAssembly and rendered with WebGL.{}",
+ "egui is an immediate mode GUI library written in Rust. egui runs natively on {}{}{}, and \
+ on the web it is compiled to WebAssembly and rendered with WebGL or WebGPU.{}",
OS_APPLE, OS_LINUX, OS_WINDOWS,
if cfg!(target_arch = "wasm32") {
" Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust."
} else {""}
));
- ui.label("egui is designed to be easy to use, portable, and fast.");
ui.add_space(12.0);
+ ui.label("egui is easy to use, portable, and fast.");
- ui.heading("Immediate mode");
+ ui.add_space(12.0);
about_immediate_mode(ui);
ui.add_space(12.0);
@@ -52,12 +62,12 @@ impl crate::View for About {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
- ui.label("egui development is sponsored by ");
+ ui.weak("egui development is sponsored by ");
ui.hyperlink_to("Rerun.io", "https://www.rerun.io/");
- ui.label(", a startup building an SDK for visualizing streams of multimodal data. ");
- ui.label("For an example of a real-world egui app, see ");
+ ui.weak(", a startup building a data platform for robotics. ");
+ ui.weak("For an example of a professional egui app, run ");
ui.hyperlink_to("rerun.io/viewer", "https://www.rerun.io/viewer");
- ui.label(" (runs in your browser).");
+ ui.weak(" (in your browser!).");
});
ui.add_space(12.0);
@@ -72,11 +82,9 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
ui.horizontal_wrapped(|ui| {
- ui.spacing_mut().item_spacing.x = 0.0;
- ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a ");
- let _ = ui.small_button("button");
- ui.label(" in egui:");
- });
+ ui.spacing_mut().item_spacing.x = 0.0;
+ ui.label("This is how you create a button in egui:");
+ });
ui.add_space(8.0);
crate::rust_view_ui(
diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs
index d2cc17448..1c7831016 100644
--- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs
+++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs
@@ -119,21 +119,30 @@ impl Default for DemoGroups {
}
impl DemoGroups {
+ pub fn about_egui_checkbox(&mut self, ui: &mut Ui, open: &mut BTreeSet) {
+ let Self { about, .. } = self;
+ let mut is_open = open.contains(about.name());
+ ui.toggle_value(&mut is_open, about.name());
+ set_open(open, about.name(), is_open);
+ }
+
pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) {
let Self {
- about,
+ about: _,
demos,
tests,
} = self;
- {
- let mut is_open = open.contains(about.name());
- ui.toggle_value(&mut is_open, about.name());
- set_open(open, about.name(), is_open);
- }
- ui.separator();
+ ui.vertical_centered(|ui| {
+ ui.strong("Demos");
+ });
demos.checkboxes(ui, open);
+
ui.separator();
+
+ ui.vertical_centered(|ui| {
+ ui.strong("Tests");
+ });
tests.checkboxes(ui, open);
}
@@ -267,22 +276,20 @@ impl DemoWindows {
.default_size(160.0)
.min_size(160.0)
.show_inside(ui, |ui| {
- ui.add_space(4.0);
- ui.vertical_centered(|ui| {
- ui.heading("✒ egui demos");
+ ui.vertical_centered_justified(|ui| {
+ ui.add_space(4.0);
+ ui.add(
+ egui::Image::new(egui::include_image!("../../data/egui-logo.svg"))
+ .max_height(32.0)
+ .tint(ui.visuals().strong_text_color()),
+ );
+
+ ui.add_space(4.0);
+
+ self.groups.about_egui_checkbox(ui, &mut self.open);
});
- ui.separator();
-
- use egui::special_emojis::GITHUB;
- ui.hyperlink_to(
- format!("{GITHUB} egui on GitHub"),
- "https://github.com/emilk/egui",
- );
- ui.hyperlink_to(
- "@ernerfeldt.bsky.social",
- "https://bsky.app/profile/ernerfeldt.bsky.social",
- );
+ ui.add_space(4.0);
ui.separator();
diff --git a/crates/egui_demo_lib/src/demo/tests/svg_test.rs b/crates/egui_demo_lib/src/demo/tests/svg_test.rs
index cd73f9150..9c188df74 100644
--- a/crates/egui_demo_lib/src/demo/tests/svg_test.rs
+++ b/crates/egui_demo_lib/src/demo/tests/svg_test.rs
@@ -30,7 +30,7 @@ impl crate::View for SvgTest {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self { color } = self;
ui.color_edit_button_srgba(color);
- let img_src = egui::include_image!("../../../data/peace.svg");
+ let img_src = egui::include_image!("../../../data/icon.svg");
// First paint a small version, sized the same as the source…
ui.add(
diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs
index 3bb814889..abe3280eb 100644
--- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs
+++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs
@@ -297,6 +297,7 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
blur_width,
round_to_pixels,
brush: _,
+ angle: _,
} = shape;
let round_to_pixels = round_to_pixels.get_or_insert(true);
diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs
index 4ac981807..3ec53a523 100644
--- a/crates/egui_demo_lib/src/demo/text_edit.rs
+++ b/crates/egui_demo_lib/src/demo/text_edit.rs
@@ -66,7 +66,8 @@ impl crate::View for TextEditDemo {
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
);
- if ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y))
+ if output.response.has_focus()
+ && ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::Y))
&& let Some(text_cursor_range) = output.cursor_range
{
use egui::TextBuffer as _;
diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs
index fe783af1c..ec5d5f3eb 100644
--- a/crates/egui_demo_lib/src/demo/widget_gallery.rs
+++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs
@@ -19,11 +19,11 @@ pub struct WidgetGallery {
color: egui::Color32,
animate_progress_bar: bool,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
#[cfg_attr(feature = "serde", serde(skip))]
- date: Option,
+ date: Option,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
with_date_button: bool,
}
@@ -39,19 +39,19 @@ impl Default for WidgetGallery {
string: Default::default(),
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
animate_progress_bar: false,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
date: None,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
with_date_button: true,
}
}
}
impl WidgetGallery {
- #[allow(clippy::allow_attributes, unused_mut)] // if not chrono
+ #[allow(clippy::allow_attributes, unused_mut)] // if not jiff
#[inline]
pub fn with_date_button(mut self, _with_date_button: bool) -> Self {
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
{
self.with_date_button = _with_date_button;
}
@@ -140,9 +140,9 @@ impl WidgetGallery {
string,
color,
animate_progress_bar,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
date,
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
with_date_button,
} = self;
@@ -229,7 +229,7 @@ impl WidgetGallery {
ui.end_row();
ui.add(doc_link_label("Image", "Image"));
- let egui_icon = egui::include_image!("../../data/icon.png");
+ let egui_icon = egui::include_image!("../../data/icon.svg");
ui.add(egui::Image::new(egui_icon.clone()));
ui.end_row();
@@ -237,17 +237,14 @@ impl WidgetGallery {
"Button with image",
"Button::image_and_text",
));
- if ui
- .add(egui::Button::image_and_text(egui_icon, "Click me!"))
- .clicked()
- {
+ if ui.button((egui_icon, "Click me!")).clicked() {
*boolean = !*boolean;
}
ui.end_row();
- #[cfg(feature = "chrono")]
+ #[cfg(feature = "jiff")]
if *with_date_button {
- let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive());
+ let date = date.get_or_insert_with(|| jiff::Zoned::now().date());
ui.add(doc_link_label_with_crate(
"egui_extras",
"DatePickerButton",
@@ -305,7 +302,7 @@ fn doc_link_label_with_crate<'a>(
}
}
-#[cfg(feature = "chrono")]
+#[cfg(feature = "jiff")]
#[cfg(test)]
mod tests {
use super::*;
@@ -317,7 +314,7 @@ mod tests {
pub fn should_match_screenshot() {
let mut demo = WidgetGallery {
// If we don't set a fixed date, the snapshot test will fail.
- date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
+ date: Some(jiff::civil::date(2024, 1, 1)),
..Default::default()
};
diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs
index 8abc69d19..d5f6a3a3c 100644
--- a/crates/egui_demo_lib/tests/misc.rs
+++ b/crates/egui_demo_lib/tests/misc.rs
@@ -59,21 +59,27 @@ fn test_italics() {
#[test]
fn test_text_selection() {
- let mut harness = Harness::builder().build_ui(|ui| {
- let visuals = ui.visuals_mut();
- visuals.selection.bg_fill = Color32::LIGHT_GREEN;
- visuals.selection.stroke.color = Color32::DARK_BLUE;
+ let mut results = egui_kittest::SnapshotResults::new();
- ui.label("Some varied ☺ text :)\nAnd it has a second line!");
- });
- harness.run();
- harness.fit_contents();
+ for (test_idx, drag_start_x) in [0.2_f32, 0.9].into_iter().enumerate() {
+ let mut harness = Harness::builder().build_ui(|ui| {
+ let visuals = ui.visuals_mut();
+ visuals.selection.bg_fill = Color32::LIGHT_GREEN;
+ visuals.selection.stroke.color = Color32::RED;
- // Drag to select text:
- let label = harness.get_by_role(Role::Label);
- harness.drag_at(label.rect().lerp_inside([0.2, 0.25]));
- harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
- harness.run();
+ ui.label("Some varied ☺ text :)\nAnd it has a second line!");
+ });
+ harness.run();
+ harness.fit_contents();
- harness.snapshot("text_selection");
+ // Drag to select text:
+ let label = harness.get_by_role(Role::Label);
+ harness.drag_at(label.rect().lerp_inside([drag_start_x, 0.25]));
+ harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
+ harness.run();
+
+ harness.snapshot(format!("text_selection_{test_idx}"));
+
+ results.extend_harness(&mut harness);
+ }
}
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png
index be63a88ea..f987b948d 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:52d2233594c4bad348f5479dcfad9576ee5fd7d49faedb6f5ba74b374cdaf3ad
-size 26977
+oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a
+size 27067
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png
index 91200ed0f..f4b7690fc 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:47f6cd15b88df83a9b2d8538e424041e661712f2e85312166a581f69f1254643
-size 26839
+oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7
+size 26887
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png
index 5d4cc4d49..8c6077a1c 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9302478abb0b86fae1af3af45d91f032272a56a2098405525d08aba4f9534644
-size 76103
+oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12
+size 76531
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png
index dda0e964c..d4c9508f9 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6927950ccbc5c81d6fbfe0a90ddd79a4306518caced14bb60debd30c7e41d326
+oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0
size 62604
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png
index f0b9c3892..40153611c 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:366d18457aabf1ebdd42fdbce8819cc67a4f59db85c452623b02ee1d0e8fc50a
-size 27817
+oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e
+size 27818
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png
index 281154b2d..f73093e3e 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3c5e803659e936268b476690427ef6a6802f477e078dc956a9d1c857b48da868
+oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6
size 114409
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png
index 6efc3f4a9..b8791ed2e 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f5e67baf0696792e50f7ab3121874d055ddee2de0514712aacbf8e135ec4743d
-size 25425
+oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc
+size 25057
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
index c6f5ae26c..b8f0440b6 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:55c5fb90736a31fbccd72be5994fc8c62b4b9da9842ad1e6bb795a1e1461a6f8
-size 98780
+oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6
+size 99297
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png
index a61363b99..a965901d5 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a4080ee1a16eea16c8f4246fe3e760ade7d0289b30d88068d1e49ffb88d88dca
-size 18280
+oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e
+size 18281
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png
index c750513c0..abd7c485b 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:600d9e0fc193396f36b599e4bfad2547128160d2e56dc2a989cb5f978d5115ae
-size 113797
+oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105
+size 113803
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png
index b57b98a53..d01e4bb7b 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3ae5e843cc9d847b0f3c4092f55b914699adb506cb807b0a97bfc4ec7d94537b
-size 22613
+oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30
+size 22563
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png
index 6a2c9cf63..0f50709d3 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b0d38cb1eebf3ce7d661d094175b425db2b9eccc5e439b14256c5d801d4454d4
-size 47285
+oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439
+size 47281
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
index e6fd0795d..1ba4655fd 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:00e4c7659cd50044d473dd2c138392f78ac7eba27f2b52bae61246f5dc5b2782
-size 23156
+oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f
+size 23148
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png
index 95c172d26..f34ac0cd4 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56b44d26946770c0878e11e3197633697ad339a7e8fcffe7279a6b4c45cd3582
-size 65384
+oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675
+size 65308
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png
index dcf935287..28fe7b683 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:34de6fd788288174e8e6f1fa48cd49dbc7b14fcf649fe302aed49c8c50178aa8
+oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959
size 33469
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png
index bbfeed26f..da06d85bf 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:96760220222bdde8dd1b3d28f089af2892403b78df8d34d3d94dc1a604387083
-size 18241
+oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791
+size 18242
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
index 32beeba37..0aab4baf8 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:48d138634343edb251435bf6f9075502b913e806e8b280f3e6012977c13af16f
-size 56753
+oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f
+size 57181
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png
index 8aa13dbfa..4b560e20a 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:888ed4281c2c779b08bc1719302b9923f542026811cff8ae91e44ea1faa25783
-size 25804
+oid sha256:fadea24444c402695db6cbc9e03aef8a0ed3c5db487a324fb255d38c14f73dce
+size 19804
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png
index 8dae8e626..2d57b2074 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:23efb79ca13367f4d8886142d015815c5bdf99c0ed243ece294a7cfd365fd166
-size 33503
+oid sha256:f6105c95470d1342f9003ab03e71243b5e18a6f225261aee94b15f8f0501572c
+size 33542
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png
index dc7dda582..5517e03ef 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5d85faf6e7fa26741eb720e74695f3c207ea15097b118c3cafe5d52d5d85ea20
-size 23666
+oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b
+size 23629
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png
index b9f2816a2..24e42cb23 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:875eb687f3a1eed52a6617e532edc5332b0a16296e2b6addac66d5bea0448b14
-size 172605
+oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c
+size 172609
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png
index 3513155f0..28864a446 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fd3573be9ba5818b4edc371095f5c23b084e6c7eaae4f2fd3a6d2de051878c9d
-size 118567
+oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d
+size 118562
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png
index c0470071c..3f72922d2 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed1be0294fb65b11c54c6dc9e4cecb383ace16dad748e3c42f2ed65b2fb05ea8
-size 75509
+oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef
+size 76022
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
index 99ab541d4..c1b6f506a 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7336c53885add09360df098b6b131323e8ad3ef0ec2b85bf022e78bc4269276a
-size 70255
+oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6
+size 70250
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png
index c86b223dd..e9b302746 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:411dd61eb182a70d46c7fc1fa0f9a4b8aeae88d08b11d5af948c5acccfa9d133
-size 60950
+oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e
+size 60947
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png
index c63f844ec..6b1a9946b 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78111e33d44a09beb9c1233dd2d5ef10103213a1c1c7df8b5e258d9684f1d93a
-size 21810
+oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3
+size 21814
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png
index 91aeb6b91..92992cd83 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e9498a706de403ee7db3603ecc896688e584fede367ed6087cdf10b798a3ab2d
-size 13698
+oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92
+size 13700
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png
index c768c6507..cdc1a43dd 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:42385da2eb74d54ba086aed973ade15f2a8d2be0c9281c05e6fb88846137bf81
-size 35870
+oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4
+size 35874
diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
index e8b44a484..96ca9949e 100644
--- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
+++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9f1b6fa0c48479606539f2d98befe1c9ee881846c0b55d7a53313962d556380d
-size 484629
+oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1
+size 483497
diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png
index aeaa46a34..5cc884a55 100644
--- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:89074b8dab103a419bc3dac743da4d8c47f435fa55b98d8aab71f6c9fb4d39de
-size 12370
+oid sha256:c8ea98c65376d9f6ac66d0a9471c4bf3add0904294e7ca1a105458b90654a2e2
+size 12476
diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png
index da72002a0..b223bbb3d 100644
--- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7bd7b54ff60859e4d4793000bef3adbec4c071063bec6bfdbde62516c4fc3478
-size 12959
+oid sha256:3793a5e83ef9bdffef99bcd8905a094acb69cde356e3a7125a544045296c3926
+size 13070
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png
index 6690a129e..96d31e11d 100644
--- a/crates/egui_demo_lib/tests/snapshots/modals_1.png
+++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b53b03212953e12915a0e41bff5f0cdea90f8f866220a01142edaeb915735a34
-size 47077
+oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6
+size 47076
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png
index f8eb85d39..3c0a88ee0 100644
--- a/crates/egui_demo_lib/tests/snapshots/modals_2.png
+++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:22e5d61a141b5a8663feb8a47371f9259d2a77fdacb1245bce411ffc85ce2cae
-size 47716
+oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3
+size 48209
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png
index c12f0b9df..28221255f 100644
--- a/crates/egui_demo_lib/tests/snapshots/modals_3.png
+++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:120558ab0c267650744bd078aeace8d4122b3569c5998602f969766131d15c44
-size 43894
+oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967
+size 44295
diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png
index ef4c45ff1..bb1935741 100644
--- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png
+++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:af92548b6c8569081a91cb772b73988d9cb342498ddf9c0c86b6963cef8eda9e
-size 43985
+oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36
+size 44055
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png
index b5367f0ed..b48827b6a 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db510af76578693c85ce78ca91224758a56f7bbf33db3221c9a4edca08b06600
-size 590547
+oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4
+size 588252
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png
index 7158a3545..d1286d6a1 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cae2b789e8afff23b7545d42a530e6c972d28736bad2bdacbc69f0e7065f85cc
-size 740660
+oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28
+size 740616
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png
index 8b9cb281f..f1892bf26 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:09d9f567ec371d60881b525ddb462d9135552db97af5921a6eb02aba40e40616
-size 971544
+oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc
+size 967860
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png
index 9f5a69154..c32762306 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3c383dd89fda6094704027074a72085591339a276d60502626d78e8e527b2e10
-size 1076719
+oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9
+size 1076746
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png
index 74760261a..d021a1e71 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0b4559541cf3259496c760a26f8d83e82179cb7e4576333682c5af49ee4a35a7
-size 1125331
+oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169
+size 1125286
diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png
index a85909178..03d4fb69a 100644
--- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png
+++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:67c8412a1e8fdbfd88f8573797fbf6fbd89c6ce783a074a8e90f7d8d9e67dd57
-size 1366351
+oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae
+size 1361407
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
index 0eb5ebd6a..1d3f1785e 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a2b7b54a1af0f5cd31bd64f0506e3035dd423314ce3389e61730fa160434fbf3
-size 45074
+oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db
+size 45107
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
index bd9942eb7..646eaa1e0 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7b66a0be67ff2d684a54c2321123521b3ad06dfe5ebffd50e89260d77efcfcc4
-size 86833
+oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606
+size 86885
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
index 64ddf5c49..e667fc387 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19320291c99a23429b114a59de4636689e281e1e68766abe2aa1e56562128e50
-size 118919
+oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44
+size 118961
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
index 105bbf285..843c3ba3b 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5edf089c00715f1456fe7838e85aadcfc42b6216a3fd95b48d9c21fc8d700cba
-size 51371
+oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f
+size 51419
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
index 035eb931d..e738e22eb 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6cd1a10639dcb323bdc3b2c43e0c35665184fc809731ced90088ee9edb9de845
-size 54577
+oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a
+size 54626
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
index 26014a12c..d46e593ba 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:87e34024f701dc93f4026213ac7eb468a2cd6d3393eb0dbec382bf58007f8e61
-size 55042
+oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085
+size 55090
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
index dcbbba2b6..31a1dd365 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d7940ff56796efb27bec66b632ff33aa2ad390c4962a711bf520aee341f035a4
-size 35968
+oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a
+size 36006
diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
index 0a3d062af..db20010e0 100644
--- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
+++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7bbd16c8aad444f0d11aacf87cf2292d494cc80a1ca46e7e8db86ca3041d35a
-size 35931
+oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0
+size 35973
diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection.png b/crates/egui_demo_lib/tests/snapshots/text_selection.png
deleted file mode 100644
index 63a4423a3..000000000
--- a/crates/egui_demo_lib/tests/snapshots/text_selection.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:0475c5ac04ab8f79b79d43cfdb985f05b61dbe90e81f898a6dc216c308a28841
-size 4707
diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png
new file mode 100644
index 000000000..7930dff48
--- /dev/null
+++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a
+size 5276
diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png
new file mode 100644
index 000000000..8691211cb
--- /dev/null
+++ b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79
+size 5301
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
index 112605454..4495bf173 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bbdc4199dee2ae853b8a240cd84528482dc6762233bd0d1249f2daa296b49487
-size 64172
+oid sha256:ef245aae271ccae628bb4171f7e601194c77fd18888ef2ea829bea75bd38b0e5
+size 64965
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
index 1b5b60c8a..7c47f522d 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f6d38b6b47839d0e4eae530d203c83971fba8a41c9caa3d5b5d89ee7ed582613
-size 150090
+oid sha256:e621561567539ff24b4d22b53b65fac6cddae71d92fccd7800a90972a6de3e0e
+size 151100
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
index 5a2b44feb..520895ff5 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0635f1564d6c9707efa68003fb8c9b6eb00408aa8f24c972e33c6c79fed5bdf
-size 59354
+oid sha256:e6c2d538be7971169bbc4473945e6815eac8c5dd6372bc1f1897a032b6bca12b
+size 59962
diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
index 81c7452e6..90311fddc 100644
--- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
+++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4288ee4a0d2229d59c31538179cdda50035a3849f69b400127e1618efe30cdc1
-size 145224
+oid sha256:d705af99624cd2824cd1f520fa05481ac67b8913feebae836db7b99ac60cb466
+size 145841
diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml
index 6639126ef..944576f08 100644
--- a/crates/egui_extras/Cargo.toml
+++ b/crates/egui_extras/Cargo.toml
@@ -15,7 +15,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
@@ -34,7 +34,7 @@ default = ["dep:mime_guess2"]
all_loaders = ["file", "http", "image", "svg", "gif", "webp"]
## Enable [`DatePickerButton`] widget.
-datepicker = ["chrono"]
+datepicker = ["jiff"]
## Add support for loading images from `file://` URIs.
file = ["dep:mime_guess2"]
@@ -83,7 +83,7 @@ profiling.workspace = true
serde = { workspace = true, optional = true }
# Date operations needed for datepicker widget
-chrono = { workspace = true, optional = true, features = ["clock", "js-sys", "std", "wasmbind"] }
+jiff = { workspace = true, optional = true, features = ["std", "tz-system", "js"] }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs
index 98aceefe2..692dc9d24 100644
--- a/crates/egui_extras/src/datepicker/button.rs
+++ b/crates/egui_extras/src/datepicker/button.rs
@@ -1,6 +1,6 @@
use super::popup::DatePickerPopup;
-use chrono::NaiveDate;
use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget};
+use jiff::civil::Date;
use std::ops::RangeInclusive;
#[derive(Default, Clone)]
@@ -11,7 +11,7 @@ pub(crate) struct DatePickerButtonState {
/// Shows a date, and will open a date picker popup when clicked.
pub struct DatePickerButton<'a> {
- selection: &'a mut NaiveDate,
+ selection: &'a mut Date,
id_salt: Option<&'a str>,
combo_boxes: bool,
arrows: bool,
@@ -20,11 +20,13 @@ pub struct DatePickerButton<'a> {
show_icon: bool,
format: String,
highlight_weekends: bool,
- start_end_years: Option>,
+ start_end_years: Option>,
+ reverse_years: bool,
+ year_scroll_to: Option,
}
impl<'a> DatePickerButton<'a> {
- pub fn new(selection: &'a mut NaiveDate) -> Self {
+ pub fn new(selection: &'a mut Date) -> Self {
Self {
selection,
id_salt: None,
@@ -36,6 +38,8 @@ impl<'a> DatePickerButton<'a> {
format: "%Y-%m-%d".to_owned(),
highlight_weekends: true,
start_end_years: None,
+ reverse_years: false,
+ year_scroll_to: None,
}
}
@@ -91,7 +95,7 @@ impl<'a> DatePickerButton<'a> {
}
/// Change the format shown on the button. (Default: %Y-%m-%d)
- /// See [`chrono::format::strftime`] for valid formats.
+ /// See [`jiff::fmt::strtime`] for valid formats.
#[inline]
pub fn format(mut self, format: impl Into) -> Self {
self.format = format.into();
@@ -111,10 +115,25 @@ impl<'a> DatePickerButton<'a> {
/// For example, if you want to provide the range of years from 2000 to 2035, you can use:
/// `start_end_years(2000..=2035)`.
#[inline]
- pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self {
+ pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self {
self.start_end_years = Some(start_end_years);
self
}
+
+ /// List years in descending order in the year dropdown. (Default: false)
+ #[inline]
+ pub fn reverse_years(mut self, reverse_years: bool) -> Self {
+ self.reverse_years = reverse_years;
+ self
+ }
+
+ /// Scroll the year dropdown to this year when the picker first opens.
+ /// Defaults to the currently selected year.
+ #[inline]
+ pub fn year_scroll_to(mut self, year: i16) -> Self {
+ self.year_scroll_to = Some(year);
+ self
+ }
}
impl Widget for DatePickerButton<'_> {
@@ -125,9 +144,9 @@ impl Widget for DatePickerButton<'_> {
.unwrap_or_default();
let mut text = if self.show_icon {
- RichText::new(format!("{} 📆", self.selection.format(&self.format)))
+ RichText::new(format!("{} 📆", self.selection.strftime(&self.format)))
} else {
- RichText::new(format!("{}", self.selection.format(&self.format)))
+ RichText::new(format!("{}", self.selection.strftime(&self.format)))
};
let visuals = ui.visuals().widgets.open;
if button_state.picker_visible {
@@ -154,7 +173,6 @@ impl Widget for DatePickerButton<'_> {
pos.x = button_response.rect.right() - width_with_padding;
}
- // Check to make sure the calendar never is displayed out of window
pos.x = pos.x.max(ui.style().spacing.window_margin.leftf());
//TODO(elwerene): Better positioning
@@ -182,6 +200,8 @@ impl Widget for DatePickerButton<'_> {
calendar_week: self.calendar_week,
highlight_weekends: self.highlight_weekends,
start_end_years: self.start_end_years,
+ reverse_years: self.reverse_years,
+ year_scroll_to: self.year_scroll_to,
}
.draw(ui)
})
diff --git a/crates/egui_extras/src/datepicker/mod.rs b/crates/egui_extras/src/datepicker/mod.rs
index 7a114b357..f1f6e58fa 100644
--- a/crates/egui_extras/src/datepicker/mod.rs
+++ b/crates/egui_extras/src/datepicker/mod.rs
@@ -4,32 +4,32 @@ mod button;
mod popup;
pub use button::DatePickerButton;
-use chrono::{Datelike as _, Duration, NaiveDate, Weekday};
+use jiff::civil::{Date, ISOWeekDate, Weekday};
#[derive(Debug)]
struct Week {
number: u8,
- days: Vec,
+ days: Vec,
}
-fn month_data(year: i32, month: u32) -> Vec {
- let first = NaiveDate::from_ymd_opt(year, month, 1).expect("Could not create NaiveDate");
+fn month_data(year: i16, month: i8) -> Vec {
+ let first = Date::new(year, month, 1).expect("Could not create Date");
let mut start = first;
- while start.weekday() != Weekday::Mon {
- start = start.checked_sub_signed(Duration::days(1)).unwrap();
+ while start.weekday() != Weekday::Monday {
+ start = start.yesterday().unwrap();
}
let mut weeks = vec![];
let mut week = vec![];
- while start < first || start.month() == first.month() || start.weekday() != Weekday::Mon {
+ while start < first || start.month() == first.month() || start.weekday() != Weekday::Monday {
week.push(start);
- if start.weekday() == Weekday::Sun {
+ if start.weekday() == Weekday::Sunday {
weeks.push(Week {
- number: start.iso_week().week() as u8,
+ number: ISOWeekDate::from(start).week() as u8,
days: std::mem::take(&mut week),
});
}
- start = start.checked_add_signed(Duration::days(1)).unwrap();
+ start = start.tomorrow().unwrap();
}
weeks
diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs
index d353307b3..5c0726e5a 100644
--- a/crates/egui_extras/src/datepicker/popup.rs
+++ b/crates/egui_extras/src/datepicker/popup.rs
@@ -1,4 +1,4 @@
-use chrono::{Datelike as _, NaiveDate, Weekday};
+use jiff::civil::{Date, Weekday};
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2};
@@ -9,40 +9,39 @@ use crate::{Column, Size, StripBuilder, TableBuilder};
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct DatePickerPopupState {
- year: i32,
- month: u32,
- day: u32,
+ year: i16,
+ month: i8,
+ day: i8,
setup: bool,
+ year_scroll_needed: bool,
}
impl DatePickerPopupState {
- fn last_day_of_month(&self) -> u32 {
- let date: NaiveDate =
- NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("Could not create NaiveDate");
- date.with_day(31)
- .map(|_| 31)
- .or_else(|| date.with_day(30).map(|_| 30))
- .or_else(|| date.with_day(29).map(|_| 29))
- .unwrap_or(28)
+ fn last_day_of_month(&self) -> i8 {
+ Date::new(self.year, self.month, 1)
+ .expect("Could not create Date")
+ .days_in_month()
}
}
pub(crate) struct DatePickerPopup<'a> {
- pub selection: &'a mut NaiveDate,
+ pub selection: &'a mut Date,
pub button_id: Id,
pub combo_boxes: bool,
pub arrows: bool,
pub calendar: bool,
pub calendar_week: bool,
pub highlight_weekends: bool,
- pub start_end_years: Option>,
+ pub start_end_years: Option>,
+ pub reverse_years: bool,
+ pub year_scroll_to: Option,
}
impl DatePickerPopup<'_> {
/// Returns `true` if user pressed `Save` button.
pub fn draw(&mut self, ui: &mut Ui) -> bool {
let id = ui.make_persistent_id("date_picker");
- let today = chrono::offset::Utc::now().date_naive();
+ let today = jiff::Zoned::now().date();
let mut popup_state = ui
.data_mut(|data| data.get_persisted::(id))
.unwrap_or_default();
@@ -51,6 +50,7 @@ impl DatePickerPopup<'_> {
popup_state.month = self.selection.month();
popup_state.day = self.selection.day();
popup_state.setup = true;
+ popup_state.year_scroll_needed = true;
ui.data_mut(|data| data.insert_persisted(id, popup_state.clone()));
}
@@ -60,7 +60,7 @@ impl DatePickerPopup<'_> {
let spacing = 2.0;
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
- ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text
+ ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
StripBuilder::new(ui)
.clip(false)
@@ -89,15 +89,30 @@ impl DatePickerPopup<'_> {
Some(range) => (*range.start(), *range.end()),
None => (today.year() - 100, today.year() + 10),
};
- for year in start_year..=end_year {
- if ui
- .selectable_value(
- &mut popup_state.year,
- year,
- year.to_string(),
- )
- .changed()
+ let scroll_to_year =
+ self.year_scroll_to.unwrap_or(popup_state.year);
+ let years: Vec = if self.reverse_years {
+ (start_year..=end_year).rev().collect()
+ } else {
+ (start_year..=end_year).collect()
+ };
+ for year in years {
+ let resp = ui.selectable_value(
+ &mut popup_state.year,
+ year,
+ year.to_string(),
+ );
+ if popup_state.year_scroll_needed
+ && year == scroll_to_year
{
+ resp.scroll_to_me(Some(Align::Center));
+ popup_state.year_scroll_needed = false;
+ ui.memory_mut(|mem| {
+ mem.data
+ .insert_persisted(id, popup_state.clone());
+ });
+ }
+ if resp.changed() {
popup_state.day = popup_state
.day
.min(popup_state.last_day_of_month());
@@ -113,7 +128,7 @@ impl DatePickerPopup<'_> {
ComboBox::from_id_salt("date_picker_month")
.selected_text(month_name(popup_state.month))
.show_ui(ui, |ui| {
- for month in 1..=12 {
+ for month in 1i8..=12 {
if ui
.selectable_value(
&mut popup_state.month,
@@ -137,7 +152,7 @@ impl DatePickerPopup<'_> {
ComboBox::from_id_salt("date_picker_day")
.selected_text(popup_state.day.to_string())
.show_ui(ui, |ui| {
- for day in 1..=popup_state.last_day_of_month() {
+ for day in 1i8..=popup_state.last_day_of_month() {
if ui
.selectable_value(
&mut popup_state.day,
@@ -314,9 +329,10 @@ impl DatePickerPopup<'_> {
&& popup_state.day == day.day()
{
ui.visuals().selection.bg_fill
- } else if (day.weekday() == Weekday::Sat
- || day.weekday() == Weekday::Sun)
- && self.highlight_weekends
+ } else if (matches!(
+ day.weekday(),
+ Weekday::Saturday | Weekday::Sunday
+ )) && self.highlight_weekends
{
if ui.visuals().dark_mode {
Color32::DARK_RED
@@ -349,7 +365,6 @@ impl DatePickerPopup<'_> {
);
if day == today {
- // Encircle today's date
let stroke = ui
.visuals()
.widgets
@@ -396,12 +411,12 @@ impl DatePickerPopup<'_> {
strip.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui.button("Save").clicked() {
- *self.selection = NaiveDate::from_ymd_opt(
+ *self.selection = Date::new(
popup_state.year,
popup_state.month,
popup_state.day,
)
- .expect("Could not create NaiveDate");
+ .expect("Could not create Date");
saved = true;
close = true;
}
@@ -424,7 +439,7 @@ impl DatePickerPopup<'_> {
}
}
-fn month_name(i: u32) -> &'static str {
+fn month_name(i: i8) -> &'static str {
match i {
1 => "January",
2 => "February",
diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs
index 19e0c95a8..99e6dd5e4 100644
--- a/crates/egui_extras/src/lib.rs
+++ b/crates/egui_extras/src/lib.rs
@@ -8,7 +8,7 @@
#![expect(clippy::manual_range_contains)]
-#[cfg(feature = "chrono")]
+#[cfg(feature = "datepicker")]
mod datepicker;
pub mod syntax_highlighting;
@@ -21,7 +21,7 @@ mod sizing;
mod strip;
mod table;
-#[cfg(feature = "chrono")]
+#[cfg(feature = "datepicker")]
pub use crate::datepicker::DatePickerButton;
pub(crate) use crate::layout::StripLayout;
diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml
index 4a083611b..f714dd8e0 100644
--- a/crates/egui_glow/Cargo.toml
+++ b/crates/egui_glow/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui_glow"
categories = ["gui", "game-development"]
keywords = ["glow", "egui", "gui", "gamedev"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"]
[lints]
workspace = true
diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml
index 33c895617..10938cd37 100644
--- a/crates/egui_kittest/Cargo.toml
+++ b/crates/egui_kittest/Cargo.toml
@@ -11,7 +11,7 @@ readme = "./README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "development-tools::testing", "accessibility"]
keywords = ["gui", "immediate", "egui", "testing", "accesskit"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -38,7 +38,7 @@ egui.workspace = true
eframe = { workspace = true, optional = true }
kittest.workspace = true
serde.workspace = true
-toml.workspace = true
+toml = {workspace = true, features = ["parse", "serde"] }
# wgpu dependencies
egui-wgpu = { workspace = true, optional = true }
diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md
index 638c61522..7b97f4e1d 100644
--- a/crates/egui_kittest/README.md
+++ b/crates/egui_kittest/README.md
@@ -97,12 +97,12 @@ You should add the following to your `.gitignore`:
* …have a low resolution to avoid growth in repo size
* …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!)
-### What do do when CI / another computer produces a different image?
+### What to do when CI / another computer produces a different image?
The default tolerance settings should be fine for almost all gui comparison tests.
However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures.
-First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilitites.
+First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilities.
Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless.
Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test.
diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs
index 94940ffff..ed68d50ee 100644
--- a/crates/egui_kittest/src/node.rs
+++ b/crates/egui_kittest/src/node.rs
@@ -98,9 +98,11 @@ impl Node<'_> {
/// This will trigger a [`accesskit::Action::Click`] action.
/// In contrast to `click()`, this can also click widgets that are not currently visible.
pub fn click_accesskit(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(
accesskit::ActionRequest {
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
action: accesskit::Action::Click,
data: None,
},
@@ -119,9 +121,11 @@ impl Node<'_> {
}
pub fn focus(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::Focus,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
@@ -162,45 +166,55 @@ impl Node<'_> {
/// Scroll the node into view.
pub fn scroll_to_me(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollIntoView,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
pub fn scroll_down(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollDown,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
pub fn scroll_up(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollUp,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
pub fn scroll_left(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollLeft,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
pub fn scroll_right(&self) {
+ let (target_node, target_tree) = self.accesskit_node.locate();
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
action: accesskit::Action::ScrollRight,
- target: self.accesskit_node.id(),
+ target_node,
+ target_tree,
data: None,
}));
}
diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs
index 3f97e0036..a9f0de9ad 100644
--- a/crates/egui_kittest/src/wgpu.rs
+++ b/crates/egui_kittest/src/wgpu.rs
@@ -17,7 +17,8 @@ pub(crate) const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
/// Default wgpu setup used for the wgpu renderer.
pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
- let mut setup = egui_wgpu::WgpuSetupCreateNew::default();
+ // No display handle needed for headless testing — we don't present to a window.
+ let mut setup = egui_wgpu::WgpuSetupCreateNew::without_display_handle();
// WebGPU not supported yet since we rely on blocking screenshots.
setup
@@ -58,6 +59,7 @@ pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
}
pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState {
+ // No display handle needed for headless testing — we don't present to a window.
let instance = pollster::block_on(setup.new_instance());
pollster::block_on(egui_wgpu::RenderState::create(
diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs
index 4289fe3a9..94617ff8b 100644
--- a/crates/egui_kittest/tests/regression_tests.rs
+++ b/crates/egui_kittest/tests/regression_tests.rs
@@ -262,3 +262,101 @@ pub fn menus_should_close_even_if_submenu_disappears() {
);
}
}
+
+fn keyboard_submenu_harness() -> Harness<'static, bool> {
+ Harness::builder()
+ .with_size(Vec2::new(400.0, 240.0))
+ .build_ui_state(
+ |ui, checked| {
+ egui::Panel::top("menu_bar").show_inside(ui, |ui| {
+ egui::MenuBar::new().ui(ui, |ui| {
+ ui.menu_button("X", |ui| {
+ ui.menu_button("Y", |ui| {
+ ui.checkbox(checked, "Goal");
+ });
+ });
+ });
+ });
+ },
+ false,
+ )
+}
+
+#[test]
+pub fn keyboard_should_open_nested_submenu() {
+ let mut harness = keyboard_submenu_harness();
+
+ harness.get_by_label("X").focus();
+ harness.run();
+
+ harness.key_press(egui::Key::Enter);
+ harness.run();
+
+ harness.get_by_label_contains("Y").focus();
+ harness.run();
+
+ harness.key_press(egui::Key::Enter);
+ harness.run();
+
+ assert!(
+ harness.query_by_label("Goal").is_some(),
+ "Expected nested submenu to open via keyboard"
+ );
+}
+
+#[test]
+pub fn keyboard_should_close_nested_submenu_with_second_enter() {
+ let mut harness = keyboard_submenu_harness();
+
+ harness.get_by_label("X").focus();
+ harness.run();
+
+ harness.key_press(egui::Key::Enter);
+ harness.run();
+
+ harness.get_by_label_contains("Y").focus();
+ harness.run();
+
+ harness.key_press(egui::Key::Enter);
+ harness.run();
+
+ assert!(
+ harness.query_by_label("Goal").is_some(),
+ "Expected nested submenu to open before close attempt"
+ );
+
+ harness.get_by_label_contains("Y").focus();
+ harness.run();
+
+ harness.key_press(egui::Key::Enter);
+ harness.run();
+
+ assert!(
+ harness.query_by_label("Goal").is_none(),
+ "Expected nested submenu to close when pressing Enter again"
+ );
+}
+
+#[test]
+pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
+ let mut harness = keyboard_submenu_harness();
+
+ harness.get_by_label("X").click();
+ harness.run();
+
+ harness.get_by_label_contains("Y").click();
+ harness.run();
+
+ assert!(
+ harness.query_by_label("Goal").is_some(),
+ "Expected submenu to remain open after pointer click on its button"
+ );
+
+ harness.get_by_label_contains("Y").click();
+ harness.run();
+
+ assert!(
+ harness.query_by_label("Goal").is_some(),
+ "Expected submenu to remain open on repeated pointer click"
+ );
+}
diff --git a/crates/egui_kittest/tests/snapshots/combobox_closed.png b/crates/egui_kittest/tests/snapshots/combobox_closed.png
index 708985b14..073ae79a3 100644
--- a/crates/egui_kittest/tests/snapshots/combobox_closed.png
+++ b/crates/egui_kittest/tests/snapshots/combobox_closed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3ca39801faddae7191ed054029263e8eca488d16e1fcbb40fed482d39fc89e8e
-size 4520
+oid sha256:00fb02e0cc2c1454d3a3dc0635be24086234c2bc5e2c9fd73741b179622e16d6
+size 4514
diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png
index 53a9c8ed1..78e1baaca 100644
--- a/crates/egui_kittest/tests/snapshots/combobox_opened.png
+++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bafe5d7129cd2137b8f7bc9662b894d959b7042c436443f835ecd421a0d9c33f
-size 8019
+oid sha256:d8757e2db9a3892d9347495ad59f14d2bd9164a9ba258375a53c9faf8176b597
+size 8016
diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png
index d9d542908..2a4621b0e 100644
--- a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png
+++ b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5b6447dd4bd6489b9b909b8240abe54db16331beaa2fb4656f01f79a08fb25f9
-size 11112
+oid sha256:38ee4acc23d9c66f127d377ac8a0dd3b683a1465ca319fba092f6d3cdff8c266
+size 11166
diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png
index 7f9b23b37..c698cdb4b 100644
--- a/crates/egui_kittest/tests/snapshots/menu/opened.png
+++ b/crates/egui_kittest/tests/snapshots/menu/opened.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4baf110e63fb104f30e9ae06e3601cfb48d7b69aec31636ca470c9ba9f6d44a9
-size 21659
+oid sha256:ac1941f5eab71bfad020132eae47e1995efa17410b7861aa9f260032e5b0472c
+size 21785
diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png
index 7c1823938..f277511c1 100644
--- a/crates/egui_kittest/tests/snapshots/menu/submenu.png
+++ b/crates/egui_kittest/tests/snapshots/menu/submenu.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93d2f336e216f371e7239b08113e659bad6c30eb299a2d8ea9537ae1c63533f0
-size 28503
+oid sha256:b1f1a4dd9de1d8405c527c7f8f04b42ed9d403d0ec507bb3ff650a6896f28df0
+size 28628
diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png
index 7e8f3f8e7..dfc2b707c 100644
--- a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png
+++ b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bf27cde6a87112a1ae832a65c80a9e34243b6ee368314379f5bf018edf439e5
-size 33239
+oid sha256:af05a9b66340e0c128d823d3935a23bcf17cfeac02a822e7277234a9c8eb26e0
+size 33393
diff --git a/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png b/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png
index fb8887d13..4d365c4c2 100644
--- a/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png
+++ b/crates/egui_kittest/tests/snapshots/override_text_color_interactive.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e8038005841dbf272375388b224dcc9fc1177b5c113d3e6f6dbc2265c88c7e60
-size 19704
+oid sha256:bf8177abaa5920e32ad4618f5296355377f475b0c0f9a95f75cbfbe468415fb8
+size 19696
diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png
index cb99dfc84..050a4a43e 100644
--- a/crates/egui_kittest/tests/snapshots/readme_example.png
+++ b/crates/egui_kittest/tests/snapshots/readme_example.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e86ed66f3ac3a81998eefbed8cb231edc6522174050676b45e9248f9e7f18533
-size 2227
+oid sha256:1dd1f5013587463f002b1becac1560876c462295dbe5dfbb1a9dbce58991e53d
+size 2209
diff --git a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png
index 9709e159e..6ceffde99 100644
--- a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png
+++ b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad75a0e568e04c20d0e3b823c7e4906c39dcd0a69a086d8e30714a9e4530d031
-size 2128
+oid sha256:cfc03625c268f0ae067d2f4521a8668b47e4bc8525350d77a480840a09cd5083
+size 2046
diff --git a/crates/egui_kittest/tests/snapshots/test_masking.png b/crates/egui_kittest/tests/snapshots/test_masking.png
index a397ceda6..5bf5dd6fa 100644
--- a/crates/egui_kittest/tests/snapshots/test_masking.png
+++ b/crates/egui_kittest/tests/snapshots/test_masking.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4216258893fae554f0ab8b3a76ef0905cacb62c70af47fa811ff6f3d99f9f3ab
-size 5619
+oid sha256:be0bd449166878ced27eff4966d1741731e926f9baabe8b590375c20103036dd
+size 5527
diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png
index 40f2e284d..e4ff540f4 100644
--- a/crates/egui_kittest/tests/snapshots/test_shrink.png
+++ b/crates/egui_kittest/tests/snapshots/test_shrink.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:21a92c29e27ef0fdec273ea2d94a2b3e74cdf380ec77f4783daeb008bd51db6d
-size 2767
+oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600
+size 2830
diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png
index 40f2e284d..e4ff540f4 100644
--- a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png
+++ b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:21a92c29e27ef0fdec273ea2d94a2b3e74cdf380ec77f4783daeb008bd51db6d
-size 2767
+oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600
+size 2830
diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png
index 86cc5a717..d6053700b 100644
--- a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png
+++ b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f9ca5f8081d677b8bff47813c4eb94319ca03855e780aed834ecc2f3d905a22c
-size 4852
+oid sha256:037f3e356d32e1a2c32767460399f919452bff0933e1db7aa113e7e2bdb083f0
+size 4927
diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml
index 416d32750..9895fffc6 100644
--- a/crates/emath/Cargo.toml
+++ b/crates/emath/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/emath"
categories = ["mathematics", "gui"]
keywords = ["math", "gui"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs
index fc26686b3..f67767e6b 100644
--- a/crates/emath/src/pos2.rs
+++ b/crates/emath/src/pos2.rs
@@ -119,6 +119,11 @@ impl Pos2 {
/// Same as `Pos2::default()`.
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
+ pub const NAN: Self = Self {
+ x: f32::NAN,
+ y: f32::NAN,
+ };
+
#[inline(always)]
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs
index c1b96ec7b..5d6a3e0fc 100644
--- a/crates/emath/src/smart_aim.rs
+++ b/crates/emath/src/smart_aim.rs
@@ -2,7 +2,7 @@
use crate::fast_midpoint;
-const NUM_DECIMALS: usize = 15;
+const NUM_DECIMALS: usize = 16;
/// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits.
///
@@ -143,6 +143,10 @@ fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 {
#[expect(clippy::approx_constant)]
#[test]
fn test_aim() {
+ assert_eq!(
+ best_in_range_f64(0.0799999999999996, 0.09999999999999995),
+ 0.08,
+ );
assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero");
diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml
index 944443792..a32bd0fe1 100644
--- a/crates/epaint/Cargo.toml
+++ b/crates/epaint/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/epaint"
categories = ["graphics", "gui"]
keywords = ["graphics", "gui", "egui"]
-include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
+include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
@@ -48,7 +48,7 @@ mint = ["emath/mint"]
rayon = ["dep:rayon"]
## Allow serialization using [`serde`](https://docs.rs/serde).
-serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"]
+serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"]
## Change Vertex layout to be compatible with unity
unity = []
@@ -62,12 +62,14 @@ emath.workspace = true
ecolor.workspace = true
ahash.workspace = true
+font-types.workspace = true
log.workspace = true
nohash-hasher.workspace = true
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
profiling.workspace = true
self_cell.workspace = true
skrifa.workspace = true
+smallvec.workspace = true
vello_cpu.workspace = true
#! ### Optional dependencies
diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs
index 8fce01f64..71cc1332e 100644
--- a/crates/epaint/src/shape_transform.rs
+++ b/crates/epaint/src/shape_transform.rs
@@ -57,6 +57,7 @@ pub fn adjust_colors(
radius: _,
fill,
stroke,
+ angle: _,
})
| Shape::Rect(RectShape {
rect: _,
@@ -67,6 +68,7 @@ pub fn adjust_colors(
round_to_pixels: _,
blur_width: _,
brush: _,
+ angle: _,
}) => {
adjust_color(fill);
adjust_color(&mut stroke.color);
diff --git a/crates/epaint/src/shapes/ellipse_shape.rs b/crates/epaint/src/shapes/ellipse_shape.rs
index 310638d0f..b436eb841 100644
--- a/crates/epaint/src/shapes/ellipse_shape.rs
+++ b/crates/epaint/src/shapes/ellipse_shape.rs
@@ -10,6 +10,9 @@ pub struct EllipseShape {
pub radius: Vec2,
pub fill: Color32,
pub stroke: Stroke,
+
+ /// Rotate ellipse by this many radians clockwise around its center.
+ pub angle: f32,
}
impl EllipseShape {
@@ -20,6 +23,7 @@ impl EllipseShape {
radius,
fill: fill_color.into(),
stroke: Default::default(),
+ angle: 0.0,
}
}
@@ -30,18 +34,38 @@ impl EllipseShape {
radius,
fill: Default::default(),
stroke: stroke.into(),
+ angle: 0.0,
}
}
+ /// Set the rotation of the ellipse (in radians, clockwise).
+ /// The ellipse rotates around its center.
+ #[inline]
+ pub fn with_angle(mut self, angle: f32) -> Self {
+ self.angle = angle;
+ self
+ }
+
+ /// Set the rotation of the ellipse (in radians, clockwise) around a custom pivot point.
+ #[inline]
+ pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self {
+ self.angle = angle;
+ let rot = emath::Rot2::from_angle(angle);
+ self.center = pivot + rot * (self.center - pivot);
+ self
+ }
+
/// The visual bounding rectangle (includes stroke width)
pub fn visual_bounding_rect(&self) -> Rect {
if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() {
Rect::NOTHING
} else {
- Rect::from_center_size(
- self.center,
+ let rect = Rect::from_center_size(
+ Pos2::ZERO,
self.radius * 2.0 + Vec2::splat(self.stroke.width),
- )
+ );
+ rect.rotate_bb(emath::Rot2::from_angle(self.angle))
+ .translate(self.center.to_vec2())
}
}
}
diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs
index 2e855d369..e0c528377 100644
--- a/crates/epaint/src/shapes/rect_shape.rs
+++ b/crates/epaint/src/shapes/rect_shape.rs
@@ -54,13 +54,16 @@ pub struct RectShape {
/// Since most rectangles do not have a texture, this is optional and in an `Arc`,
/// so that [`RectShape`] is kept small..
pub brush: Option>,
+
+ /// Rotate rectangle by this many radians clockwise around its center.
+ pub angle: f32,
}
#[test]
fn rect_shape_size() {
assert_eq!(
std::mem::size_of::(),
- 48,
+ 56,
"RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
@@ -88,6 +91,7 @@ impl RectShape {
round_to_pixels: None,
blur_width: 0.0,
brush: Default::default(),
+ angle: 0.0,
}
}
@@ -157,6 +161,25 @@ impl RectShape {
self
}
+ /// Set the rotation of the rectangle (in radians, clockwise).
+ /// The rectangle rotates around its center.
+ #[inline]
+ pub fn with_angle(mut self, angle: f32) -> Self {
+ self.angle = angle;
+ self
+ }
+
+ /// Set the rotation of the rectangle (in radians, clockwise) around a custom pivot point.
+ #[inline]
+ pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self {
+ self.angle = angle;
+ let rot = emath::Rot2::from_angle(angle);
+ let center = self.rect.center();
+ let new_center = pivot + rot * (center - pivot);
+ self.rect = self.rect.translate(new_center - center);
+ self
+ }
+
/// The visual bounding rectangle (includes stroke width)
#[inline]
pub fn visual_bounding_rect(&self) -> Rect {
@@ -168,7 +191,17 @@ impl RectShape {
StrokeKind::Middle => self.stroke.width / 2.0,
StrokeKind::Outside => self.stroke.width,
};
- self.rect.expand(expand + self.blur_width / 2.0)
+ let expanded = self.rect.expand(expand + self.blur_width / 2.0);
+ if self.angle == 0.0 {
+ expanded
+ } else {
+ // Rotate around the rectangle's center and compute bounding box
+ let center = self.rect.center();
+ let rect_relative = Rect::from_center_size(Pos2::ZERO, expanded.size());
+ rect_relative
+ .rotate_bb(emath::Rot2::from_angle(self.angle))
+ .translate(center.to_vec2())
+ }
}
}
diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs
index 9529765ac..9256ae16e 100644
--- a/crates/epaint/src/tessellator.rs
+++ b/crates/epaint/src/tessellator.rs
@@ -1546,6 +1546,7 @@ impl Tessellator {
radius,
fill,
stroke,
+ angle,
} = shape;
if radius.x <= 0.0 || radius.y <= 0.0 {
@@ -1596,6 +1597,14 @@ impl Tessellator {
points.push(center + Vec2::new(0.0, -radius.y));
points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y)));
+ // Apply rotation if angle is non-zero
+ if angle != 0.0 {
+ let rot = emath::Rot2::from_angle(angle);
+ for point in &mut points {
+ *point = center + rot * (*point - center);
+ }
+ }
+
let path_stroke = PathStroke::from(stroke).outside();
self.scratchpad_path.clear();
self.scratchpad_path.add_line_loop(&points);
@@ -1773,6 +1782,7 @@ impl Tessellator {
round_to_pixels,
mut blur_width,
brush: _, // brush is extracted on its own, because it is not Copy
+ angle,
} = *rect_shape;
let mut corner_radius = CornerRadiusF32::from(corner_radius);
@@ -1940,6 +1950,16 @@ impl Tessellator {
let path = &mut self.scratchpad_path;
path.clear();
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
+
+ // Apply rotation if angle is non-zero
+ if angle != 0.0 {
+ let rot = emath::Rot2::from_angle(angle);
+ let center = rect.center();
+ for point in &mut self.scratchpad_points {
+ *point = center + rot * (*point - center);
+ }
+ }
+
path.add_line_loop(&self.scratchpad_points);
let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind);
diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs
index 96c0467e6..c53ce6f87 100644
--- a/crates/epaint/src/text/font.rs
+++ b/crates/epaint/src/text/font.rs
@@ -12,7 +12,7 @@ use vello_cpu::{color, kurbo};
use crate::{
TextOptions, TextureAtlas,
text::{
- FontTweak,
+ FontTweak, VariationCoords,
fonts::{Blob, CachedFamily, FontFaceKey},
},
};
@@ -145,8 +145,8 @@ struct GlyphCacheKey(u64);
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
impl GlyphCacheKey {
- fn new(glyph_id: skrifa::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
- let ScaledMetrics {
+ fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
+ let StyledMetrics {
pixels_per_point,
px_scale_factor,
..
@@ -197,10 +197,10 @@ impl FontCell {
fn allocate_glyph_uncached(
&mut self,
atlas: &mut TextureAtlas,
- metrics: &ScaledMetrics,
+ metrics: &StyledMetrics,
glyph_info: &GlyphInfo,
bin: SubpixelBin,
- location: &skrifa::instance::Location,
+ location: skrifa::instance::LocationRef<'_>,
) -> Option {
let glyph_id = glyph_info.id?;
@@ -337,8 +337,6 @@ pub struct FontFace {
font: FontCell,
tweak: FontTweak,
- /// Variable font location (for weight axis, etc.)
- location: skrifa::instance::Location,
glyph_info_cache: ahash::HashMap,
glyph_alloc_cache: ahash::HashMap,
@@ -355,7 +353,6 @@ impl FontFace {
font_data: Blob,
index: u32,
tweak: FontTweak,
- preferred_weight: Option,
) -> Result> {
let font = FontCell::try_new(font_data, |font_data| {
let skrifa_font =
@@ -401,44 +398,10 @@ impl FontFace {
})
})?;
- // Use preferred_weight if provided, otherwise try to read from the OS/2 table or fvar default
- let weight = preferred_weight.or_else(|| {
- // First try OS/2 table
- if let Some(w) = font
- .borrow_dependent()
- .skrifa
- .os2()
- .ok()
- .map(|os2| os2.us_weight_class())
- {
- return Some(w);
- }
- // If no OS/2 or preferred_weight, try to get default from variable font's fvar table
- font.borrow_dependent()
- .skrifa
- .axes()
- .iter()
- .find(|axis| axis.tag() == skrifa::raw::types::Tag::new(b"wght"))
- .map(|axis| axis.default_value() as u16)
- });
-
- // Create location for variable font with weight axis
- // If weight is provided (either from preferred_weight, OS/2, or fvar default), use it
- // Otherwise fall back to Location::default() which uses all axis defaults
- let location = if let Some(w) = weight {
- font.borrow_dependent()
- .skrifa
- .axes()
- .location([("wght", w as f32)])
- } else {
- skrifa::instance::Location::default()
- };
-
Ok(Self {
name,
font,
tweak,
- location,
glyph_info_cache: Default::default(),
glyph_alloc_cache: Default::default(),
#[cfg(target_arch = "wasm32")]
@@ -544,7 +507,7 @@ impl FontFace {
#[inline]
pub(super) fn pair_kerning_pixels(
&self,
- metrics: &ScaledMetrics,
+ metrics: &StyledMetrics,
last_glyph_id: skrifa::GlyphId,
glyph_id: skrifa::GlyphId,
) -> f32 {
@@ -566,7 +529,7 @@ impl FontFace {
#[inline]
pub fn pair_kerning(
&self,
- metrics: &ScaledMetrics,
+ metrics: &StyledMetrics,
last_glyph_id: skrifa::GlyphId,
glyph_id: skrifa::GlyphId,
) -> f32 {
@@ -574,7 +537,12 @@ impl FontFace {
}
#[inline(always)]
- pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
+ pub fn styled_metrics(
+ &self,
+ pixels_per_point: f32,
+ font_size: f32,
+ coords: &VariationCoords,
+ ) -> StyledMetrics {
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
let font_data = self.font.borrow_dependent();
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
@@ -588,20 +556,32 @@ impl FontFace {
+ self.tweak.y_offset)
.round_ui();
- ScaledMetrics {
+ let axes = font_data.skrifa.axes();
+ // Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the
+ // argument (probably from TextFormat).
+ let settings = self
+ .tweak
+ .coords
+ .as_ref()
+ .iter()
+ .chain(coords.as_ref().iter());
+ let location = axes.location(settings);
+
+ StyledMetrics {
pixels_per_point,
px_scale_factor,
scale,
y_offset_in_points,
ascent,
row_height: ascent - descent + line_gap,
+ location,
}
}
pub fn allocate_glyph(
&mut self,
atlas: &mut TextureAtlas,
- metrics: &ScaledMetrics,
+ metrics: &StyledMetrics,
glyph_info: GlyphInfo,
chr: char,
h_pos: f32,
@@ -653,7 +633,7 @@ impl FontFace {
// Allocate the glyph
let allocation = self
.font
- .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, &self.location)
+ .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
.unwrap_or_default();
// Insert into cache
@@ -711,12 +691,17 @@ impl Font<'_> {
})
}
- pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
+ pub fn styled_metrics(
+ &self,
+ pixels_per_point: f32,
+ font_size: f32,
+ coords: &VariationCoords,
+ ) -> StyledMetrics {
self.cached_family
.fonts
.first()
.and_then(|key| self.fonts_by_id.get(key))
- .map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
+ .map(|font_face| font_face.styled_metrics(pixels_per_point, font_size, coords))
.unwrap_or_default()
}
@@ -759,8 +744,8 @@ impl Font<'_> {
}
/// Metrics for a font at a specific screen-space scale.
-#[derive(Clone, Copy, Debug, PartialEq, Default)]
-pub struct ScaledMetrics {
+#[derive(Clone, Debug, PartialEq, Default)]
+pub struct StyledMetrics {
/// The DPI part of the screen-space scale.
pub pixels_per_point: f32,
@@ -784,6 +769,9 @@ pub struct ScaledMetrics {
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub row_height: f32,
+
+ /// Resolved variation coordinates.
+ pub location: skrifa::instance::Location,
}
/// Code points that will always be invisible (zero width).
diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs
index 709d45859..a4e30af51 100644
--- a/crates/epaint/src/text/fonts.rs
+++ b/crates/epaint/src/text/fonts.rs
@@ -10,7 +10,7 @@ use std::{
use crate::{
TextureAtlas,
text::{
- Galley, LayoutJob, LayoutSection, TextOptions,
+ Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
font::{Font, FontFace, GlyphInfo},
},
};
@@ -125,12 +125,6 @@ pub struct FontData {
/// Extra scale and vertical tweak to apply to all text of this font.
pub tweak: FontTweak,
-
- /// The font weight (100-900), if available.
- /// Standard values: 100 (Thin), 200 (Extra Light), 300 (Light), 400 (Regular),
- /// 500 (Medium), 600 (Semi Bold), 700 (Bold), 800 (Extra Bold), 900 (Black).
- /// `None` if the weight could not be determined.
- pub weight: Option,
}
impl FontData {
@@ -139,7 +133,6 @@ impl FontData {
font: Cow::Borrowed(font),
index: 0,
tweak: Default::default(),
- weight: None,
}
}
@@ -148,43 +141,12 @@ impl FontData {
font: Cow::Owned(font),
index: 0,
tweak: Default::default(),
- weight: None,
}
}
pub fn tweak(self, tweak: FontTweak) -> Self {
Self { tweak, ..self }
}
-
- /// Set the font weight (100-900).
- ///
- /// This is typically read automatically from the font file when loaded,
- /// but can be overridden manually if needed.
- ///
- /// Standard weight values:
- /// - 100: Thin
- /// - 200: Extra Light
- /// - 300: Light
- /// - 400: Regular/Normal
- /// - 500: Medium
- /// - 600: Semi Bold
- /// - 700: Bold
- /// - 800: Extra Bold
- /// - 900: Black
- ///
- /// # Example
- /// ```
- /// # use epaint::text::FontData;
- /// let font_data = FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf"))
- /// .weight(300); // Override to Light weight
- /// assert_eq!(font_data.weight, Some(300));
- /// ```
- pub fn weight(self, weight: u16) -> Self {
- Self {
- weight: Some(weight),
- ..self
- }
- }
}
impl AsRef<[u8]> for FontData {
@@ -196,7 +158,7 @@ impl AsRef<[u8]> for FontData {
// ----------------------------------------------------------------------------
/// Extra scale and vertical tweak to apply to all text of a certain font.
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct FontTweak {
/// Scale the font's glyphs by this much.
@@ -228,6 +190,9 @@ pub struct FontTweak {
///
/// `None` means use the global setting.
pub hinting_override: Option,
+
+ /// Override the font's default variation coordinates.
+ pub coords: VariationCoords,
}
impl Default for FontTweak {
@@ -237,6 +202,7 @@ impl Default for FontTweak {
y_offset_factor: 0.0,
y_offset: 0.0,
hinting_override: None,
+ coords: VariationCoords::default(),
}
}
}
@@ -718,7 +684,12 @@ impl FontsView<'_> {
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
self.fonts
.font(&font_id.family)
- .scaled_metrics(self.pixels_per_point, font_id.size)
+ .styled_metrics(
+ self.pixels_per_point,
+ font_id.size,
+ // TODO(valadaptive): use font variation coords when calculating row height
+ &VariationCoords::default(),
+ )
.row_height
}
@@ -824,15 +795,13 @@ impl FontsImpl {
let mut fonts_by_id: nohash_hasher::IntMap = Default::default();
let mut fonts_by_name: ahash::HashMap = Default::default();
for (name, font_data) in &definitions.font_data {
- let tweak = font_data.tweak;
let blob = blob_from_font_data(font_data);
let font_face = FontFace::new(
options,
name.clone(),
blob,
font_data.index,
- tweak,
- font_data.weight,
+ font_data.tweak.clone(),
)
.unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
let key = FontFaceKey::new();
diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs
index 5b2400646..0233c1c58 100644
--- a/crates/epaint/src/text/text_layout.rs
+++ b/crates/epaint/src/text/text_layout.rs
@@ -8,7 +8,7 @@ use crate::{
Color32, Mesh, Stroke, Vertex,
stroke::PathStroke,
text::{
- font::{ScaledMetrics, is_cjk, is_cjk_break_allowed},
+ font::{StyledMetrics, is_cjk, is_cjk_break_allowed},
fonts::FontFaceKey,
},
};
@@ -114,7 +114,7 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc)
let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs);
let mut elided = false;
- let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
+ let mut rows = rows_from_paragraphs(paragraphs, &job, pixels_per_point, &mut elided);
if elided && let Some(last_placed) = rows.last_mut() {
let last_row = Arc::make_mut(&mut last_placed.row);
replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row);
@@ -160,7 +160,7 @@ fn layout_section(
} = section;
let mut font = fonts.font(&format.font_id.family);
let font_size = format.font_id.size;
- let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
+ let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
let line_height = section
.format
.line_height
@@ -178,7 +178,7 @@ fn layout_section(
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
let mut current_font = FontFaceKey::INVALID;
- let mut current_font_face_metrics = ScaledMetrics::default();
+ let mut current_font_face_metrics = StyledMetrics::default();
for chr in job.text[byte_range.clone()].chars() {
if job.break_on_newline && chr == '\n' {
@@ -192,7 +192,9 @@ fn layout_section(
current_font = font_id;
current_font_face_metrics = font_face
.as_ref()
- .map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
+ .map(|font_face| {
+ font_face.styled_metrics(pixels_per_point, font_size, &format.coords)
+ })
.unwrap_or_default();
}
@@ -252,11 +254,12 @@ fn calculate_intrinsic_size(
) -> Vec2 {
let mut intrinsic_size = Vec2::ZERO;
for (idx, paragraph) in paragraphs.iter().enumerate() {
- let width = paragraph
- .glyphs
- .last()
- .map(|l| l.max_x())
- .unwrap_or_default();
+ // Use the precise cursor position instead of `last_glyph.max_x()`,
+ // because glyph positions are pixel-snapped but the cursor tracks
+ // the exact subpixel advance. This ensures that when two galleys are
+ // placed side-by-side, the gap matches what it would be within a
+ // single galley.
+ let width = paragraph.cursor_x_px / point_scale.pixels_per_point;
intrinsic_size.x = f32::max(intrinsic_size.x, width);
let mut height = paragraph
@@ -277,6 +280,7 @@ fn calculate_intrinsic_size(
fn rows_from_paragraphs(
paragraphs: Vec,
job: &LayoutJob,
+ pixels_per_point: f32,
elided: &mut bool,
) -> Vec {
let num_paragraphs = paragraphs.len();
@@ -303,8 +307,11 @@ fn rows_from_paragraphs(
ends_with_newline: !is_last_paragraph,
});
} else {
- let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x();
- if paragraph_max_x <= job.effective_wrap_width() {
+ // Use precise cursor position for width instead of pixel-snapped
+ // `last_glyph.max_x()`, so that side-by-side galleys have the same
+ // spacing as characters within a single galley.
+ let paragraph_width = paragraph.cursor_x_px / pixels_per_point;
+ if paragraph_width <= job.effective_wrap_width() {
// Early-out optimization: the whole paragraph fits on one row.
rows.push(PlacedRow {
pos: pos2(0.0, f32::NAN),
@@ -312,7 +319,7 @@ fn rows_from_paragraphs(
section_index_at_start: paragraph.section_index_at_start,
glyphs: paragraph.glyphs,
visuals: Default::default(),
- size: vec2(paragraph_max_x, 0.0),
+ size: vec2(paragraph_width, 0.0),
}),
ends_with_newline: !is_last_paragraph,
});
@@ -468,7 +475,7 @@ fn replace_last_glyph_with_overflow_character(
let mut font_face = font.fonts_by_id.get_mut(&font_id);
let font_face_metrics = font_face
.as_mut()
- .map(|f| f.scaled_metrics(pixels_per_point, font_size))
+ .map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords))
.unwrap_or_default();
let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
@@ -495,7 +502,9 @@ fn replace_last_glyph_with_overflow_character(
let replacement_glyph_width = font_face
.as_mut()
.and_then(|f| f.glyph_info(overflow_character))
- .map(|i| i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor)
+ .map(|i| {
+ i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point
+ })
.unwrap_or_default();
// Check if we're within width budget:
@@ -517,7 +526,8 @@ fn replace_last_glyph_with_overflow_character(
})
.unwrap_or_default();
- let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
+ let font_metrics =
+ font.styled_metrics(pixels_per_point, font_size, §ion.format.coords);
let line_height = section
.format
.line_height
@@ -1166,6 +1176,42 @@ mod tests {
assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x());
}
+ #[test]
+ fn test_truncate_with_pixels_per_point() {
+ let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
+
+ for pixels_per_point in [
+ 0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0,
+ ] {
+ for ch in ['W', 'A', 'n', 't', 'i'] {
+ let target_width = 50.0;
+ let text = (0..20).map(|_| ch).collect::();
+
+ let mut job = LayoutJob::single_section(text, TextFormat::default());
+ job.wrap.max_width = target_width;
+ job.wrap.max_rows = 1;
+ let elided_galley = layout(&mut fonts, pixels_per_point, job.into());
+ assert!(elided_galley.elided);
+
+ let test_galley = layout(
+ &mut fonts,
+ pixels_per_point,
+ Arc::new(LayoutJob::single_section(
+ (0..elided_galley.rows[0].char_count_excluding_newline())
+ .map(|_| ch)
+ .chain(std::iter::once('…'))
+ .collect::(),
+ TextFormat::default(),
+ )),
+ );
+
+ assert!(elided_galley.size().x >= 0.0);
+ assert!(elided_galley.size().x <= target_width);
+ assert!(test_galley.size().x > target_width);
+ }
+ }
+ }
+
#[test]
fn test_empty_row() {
let pixels_per_point = 1.0;
@@ -1174,7 +1220,7 @@ mod tests {
let font_id = FontId::default();
let font_height = fonts
.font(&font_id.family)
- .scaled_metrics(pixels_per_point, font_id.size)
+ .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
.row_height;
let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
@@ -1207,7 +1253,7 @@ mod tests {
let font_id = FontId::default();
let font_height = fonts
.font(&font_id.family)
- .scaled_metrics(pixels_per_point, font_id.size)
+ .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
.row_height;
let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs
index 3e8a53d9e..22fb03c57 100644
--- a/crates/epaint/src/text/text_layout_types.rs
+++ b/crates/epaint/src/text/text_layout_types.rs
@@ -1,5 +1,5 @@
-use std::ops::Range;
use std::sync::Arc;
+use std::{ops::Range, str::FromStr as _};
use super::{
cursor::{CCursor, LayoutCursor},
@@ -7,6 +7,8 @@ use super::{
};
use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
+pub use font_types::Tag;
+use smallvec::SmallVec;
/// Describes the task of laying out text.
///
@@ -257,6 +259,107 @@ impl std::hash::Hash for LayoutSection {
// ----------------------------------------------------------------------------
+/// Helper trait for all types that can be parsed as a [`font_types::Tag`].
+pub trait IntoTag {
+ fn into_tag(self) -> font_types::Tag;
+}
+
+impl IntoTag for font_types::Tag {
+ #[inline(always)]
+ fn into_tag(self) -> font_types::Tag {
+ self
+ }
+}
+
+impl IntoTag for u32 {
+ #[inline(always)]
+ fn into_tag(self) -> font_types::Tag {
+ font_types::Tag::from_u32(self)
+ }
+}
+
+impl IntoTag for [u8; 4] {
+ #[inline(always)]
+ fn into_tag(self) -> font_types::Tag {
+ font_types::Tag::new_checked(&self).expect("Invalid variation axis tag")
+ }
+}
+
+impl IntoTag for &[u8; 4] {
+ #[inline(always)]
+ fn into_tag(self) -> font_types::Tag {
+ font_types::Tag::new_checked(self).expect("Invalid variation axis tag")
+ }
+}
+
+impl IntoTag for &str {
+ #[inline(always)]
+ fn into_tag(self) -> font_types::Tag {
+ font_types::Tag::from_str(self).expect("Invalid variation axis tag")
+ }
+}
+
+/// List of font variation coordinates by axis tag. If more than one coordinate for a given axis is provided, the last
+/// one added is used.
+#[derive(Clone, Debug, PartialEq, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
+pub struct VariationCoords(SmallVec<[(font_types::Tag, f32); 2]>);
+
+impl VariationCoords {
+ /// Create a list of variation coordinates from a sequence of (tag, value) pairs.
+ ///
+ /// ## Example:
+ /// ```
+ /// use epaint::text::VariationCoords;
+ ///
+ /// let coords = VariationCoords::new([
+ /// (b"wght", 500.0),
+ /// (b"wdth", 75.0),
+ /// ]);
+ /// ```
+ pub fn new(values: impl IntoIterator
- ) -> Self {
+ Self(values.into_iter().map(|(t, c)| (t.into_tag(), c)).collect())
+ }
+
+ /// Add a variation coordinate to the list.
+ #[inline(always)]
+ pub fn push(&mut self, tag: impl IntoTag, coord: f32) {
+ self.0.push((tag.into_tag(), coord));
+ }
+
+ /// Remove the coordinate at the given index.
+ pub fn remove(&mut self, index: usize) {
+ self.0.remove(index);
+ }
+
+ pub fn clear(&mut self) {
+ self.0.clear();
+ }
+}
+
+impl AsRef<[(font_types::Tag, f32)]> for VariationCoords {
+ #[inline(always)]
+ fn as_ref(&self) -> &[(font_types::Tag, f32)] {
+ &self.0
+ }
+}
+
+impl AsMut<[(font_types::Tag, f32)]> for VariationCoords {
+ fn as_mut(&mut self) -> &mut [(font_types::Tag, f32)] {
+ &mut self.0
+ }
+}
+
+impl std::hash::Hash for VariationCoords {
+ fn hash(&self, state: &mut H) {
+ self.0.len().hash(state);
+ for (tag, coord) in &self.0 {
+ tag.hash(state);
+ OrderedFloat(*coord).hash(state);
+ }
+ }
+}
+
/// Formatting option for a section of text.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -287,6 +390,8 @@ pub struct TextFormat {
/// Default: 1.0
pub expand_bg: f32,
+ pub coords: VariationCoords,
+
pub italics: bool,
pub underline: Stroke,
@@ -315,6 +420,7 @@ impl Default for TextFormat {
color: Color32::GRAY,
background: Color32::TRANSPARENT,
expand_bg: 1.0,
+ coords: VariationCoords::default(),
italics: false,
underline: Stroke::NONE,
strikethrough: Stroke::NONE,
@@ -333,6 +439,7 @@ impl std::hash::Hash for TextFormat {
color,
background,
expand_bg,
+ coords,
italics,
underline,
strikethrough,
@@ -346,6 +453,7 @@ impl std::hash::Hash for TextFormat {
color.hash(state);
background.hash(state);
emath::OrderedFloat(*expand_bg).hash(state);
+ coords.hash(state);
italics.hash(state);
underline.hash(state);
strikethrough.hash(state);
@@ -934,12 +1042,12 @@ impl Galley {
}
/// Returns a 0-width Rect.
- fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
+ pub fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
let Some(row) = self.rows.get(layout_cursor.row) else {
return self.end_pos();
};
- let x = row.x_offset(layout_cursor.column);
+ let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left();
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
}
@@ -984,7 +1092,7 @@ impl Galley {
if is_pos_within_row || y_dist < best_y_dist {
best_y_dist = y_dist;
// char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
- let column = row.char_at(pos.x - row.pos.x);
+ let column = row.char_at(pos.x - row.pos.x + self.rect.left());
let prefer_next_row = column < row.char_count_excluding_newline();
cursor = CCursor {
index: ccursor_index + column,
diff --git a/crates/epaint_default_fonts/Cargo.toml b/crates/epaint_default_fonts/Cargo.toml
index 588719871..43f6fedfe 100644
--- a/crates/epaint_default_fonts/Cargo.toml
+++ b/crates/epaint_default_fonts/Cargo.toml
@@ -12,8 +12,8 @@ repository = "https://github.com/emilk/egui/tree/main/crates/epaint_default_font
categories = ["graphics", "gui"]
keywords = ["graphics", "gui", "egui"]
include = [
- "../LICENSE-APACHE",
- "../LICENSE-MIT",
+ "../../LICENSE-APACHE",
+ "../../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"fonts/*.ttf",
diff --git a/crates/epaint_default_fonts/emoji-icon-font.ttf b/crates/epaint_default_fonts/emoji-icon-font.ttf
deleted file mode 100644
index 0f29dfed8..000000000
Binary files a/crates/epaint_default_fonts/emoji-icon-font.ttf and /dev/null differ
diff --git a/crates/epaint_default_fonts/fonts/emoji-icon-font.ttf b/crates/epaint_default_fonts/fonts/emoji-icon-font.ttf
index 806408328..0f29dfed8 100644
Binary files a/crates/epaint_default_fonts/fonts/emoji-icon-font.ttf and b/crates/epaint_default_fonts/fonts/emoji-icon-font.ttf differ
diff --git a/deny.toml b/deny.toml
index e07d476fa..2d3231535 100644
--- a/deny.toml
+++ b/deny.toml
@@ -53,18 +53,18 @@ skip = [
{ name = "core-graphics-types" }, # version conflict between winit and wgpu ecosystems
{ name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2
{ name = "kurbo" }, # Old version because of resvg
- { name = "quick-xml" }, # old version via wayland-scanner
{ name = "redox_syscall" }, # old version via winit
{ name = "rustc-hash" }, # Small enough
{ name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x
{ name = "thiserror-impl" }, # same as above
- { name = "windows-sys" }, # mostly hopeless to avoid
- { name = "zerocopy" }, # Small enough
+ { name = "toml_datetime" }, # required while eco-system updates to toml 1.0
]
skip-tree = [
{ name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15
{ name = "rfd" }, # example dependency
{ name = "windows" }, # the ecosystem is currently transitioning from 0.58 to 0.61
+ { name = "phf" }, # mime_guess2, unicode_names2 -> 0.11.3; accesskit -> 0.13.1
+ { name = "windows-sys" }, # mostly hopeless to avoid
]
@@ -79,6 +79,7 @@ allow = [
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained
"CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/
+ "CDLA-Permissive-2.0", # https://spdx.org/licenses/CDLA-Permissive-2.0.html
"ISC", # https://www.tldrlegal.com/license/isc-license
"MIT-0", # https://choosealicense.com/licenses/mit-0/
"MIT", # https://tldrlegal.com/license/mit-license
diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml
index 7c8444f99..ae484783c 100644
--- a/examples/hello_android/Cargo.toml
+++ b/examples/hello_android/Cargo.toml
@@ -22,7 +22,7 @@ eframe = { workspace = true, default-features = false, features = [
"glow",
"android-native-activity",
] }
-egui_demo_lib = { workspace = true, features = ["chrono"] }
+egui_demo_lib = { workspace = true, features = ["jiff"] }
# For image support:
egui_extras = { workspace = true, features = ["default", "image"] }
diff --git a/scripts/check.sh b/scripts/check.sh
index 71dc38386..ce0b3b8d5 100755
--- a/scripts/check.sh
+++ b/scripts/check.sh
@@ -29,7 +29,6 @@ cargo check --quiet --all-targets
cargo check --quiet --all-targets --all-features
cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown
cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
-# TODO(#5297) re-enable --all-features once the tests work with the unity feature
cargo test --quiet --all-targets --all-features
cargo test --quiet --doc # slow - checks all doc-tests
diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh
index f51258855..5ba11b665 100755
--- a/scripts/setup_web.sh
+++ b/scripts/setup_web.sh
@@ -10,6 +10,6 @@ rustup target add wasm32-unknown-unknown
# For generating JS bindings:
# Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml
-if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.100'; then
- cargo install --force --quiet wasm-bindgen-cli --version 0.2.100
+if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.108'; then
+ cargo install --force --quiet wasm-bindgen-cli --version 0.2.108 --locked
fi
diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs
index 9e76394cb..f32ff7ff3 100644
--- a/tests/egui_tests/tests/regression_tests.rs
+++ b/tests/egui_tests/tests/regression_tests.rs
@@ -1,5 +1,14 @@
+use std::sync::Arc;
+
+use egui::ScrollArea;
use egui::accesskit::Role;
-use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image};
+use egui::epaint::Shape;
+use egui::style::ScrollAnimation;
+use egui::text::{LayoutJob, TextWrapping};
+use egui::{
+ Align, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer,
+ TextFormat, TextWrapMode, Ui, include_image, vec2,
+};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable as _;
@@ -62,6 +71,114 @@ fn text_edit_rtl() {
}
}
+#[test]
+fn text_edit_halign() {
+ let mut harness = Harness::builder().with_size((212.0, 212.0)).build_ui(|ui| {
+ ui.spacing_mut().item_spacing = vec2(2.0, 2.0);
+
+ fn layouter(halign: Align) -> impl FnMut(&Ui, &dyn TextBuffer, f32) -> Arc {
+ move |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
+ let mut job = LayoutJob {
+ wrap: TextWrapping {
+ max_rows: 4,
+ max_width: wrap_width,
+ ..Default::default()
+ },
+ halign,
+ ..Default::default()
+ };
+ job.append(
+ buf.as_str(),
+ 0.0,
+ TextFormat::simple(FontId::new(13.0, FontFamily::Proportional), Color32::GRAY),
+ );
+ ui.fonts_mut(|f| f.layout_job(job))
+ }
+ }
+
+ for widget_alignment in [Align::Min, Align::Center, Align::Max] {
+ ui.horizontal(|ui| {
+ for text_alignment in [Align::LEFT, Align::Center, Align::RIGHT] {
+ ui.add_sized(
+ vec2(64.0, 64.0),
+ egui::TextEdit::multiline(&mut format!(
+ "{widget_alignment:?}\n+\n{text_alignment:?}",
+ ))
+ .layouter(&mut layouter(text_alignment))
+ .vertical_align(widget_alignment)
+ .horizontal_align(widget_alignment),
+ );
+ }
+ });
+ }
+ });
+
+ harness.get_by_value("Center\n+\nCenter").focus();
+ harness.step();
+ harness.snapshot("text_edit_halign");
+}
+
+#[test]
+fn text_edit_delay() {
+ let mut text = String::new();
+ let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| {
+ ui.style_mut().scroll_animation = ScrollAnimation::none();
+ ui.add(egui::TextEdit::singleline(&mut text).hint_text("Write something"));
+ });
+
+ harness.get_by_role(Role::TextInput).focus();
+ harness.step();
+ harness.snapshot("text_edit_delay_0_empty");
+
+ harness.get_by_role(Role::TextInput).type_text("h");
+
+ // When the text is empty, and we show the hint text, there is a frame delay.
+ harness.step();
+ harness.snapshot("text_edit_delay_1_h_invisible");
+
+ // Now it should be visible
+ harness.step();
+ harness.snapshot("text_edit_delay_2_h_visible");
+
+ harness.get_by_role(Role::TextInput).type_text("i");
+
+ // The "i" should immediately be visible without a delay
+ harness.step();
+ harness.snapshot("text_edit_delay_3_i_visible");
+
+ // The next frame should exactly match the previous one
+ harness.step();
+ harness.snapshot("text_edit_delay_4_i_visible");
+}
+
+#[test]
+fn text_edit_scroll() {
+ let mut text = "1\n2\n3\n4\n".to_owned();
+ let mut harness = Harness::builder().build_ui(|ui| {
+ ScrollArea::vertical().max_height(40.0).show(ui, |ui| {
+ ui.add(
+ egui::TextEdit::multiline(&mut text)
+ .desired_rows(2)
+ .hint_text("Write something"),
+ );
+ });
+ });
+
+ harness.fit_contents();
+
+ harness.get_by_role(Role::MultilineTextInput).focus();
+ harness.step();
+ harness.snapshot("text_edit_scroll_0_focus");
+
+ harness
+ .get_by_role(Role::MultilineTextInput)
+ .type_text("5\n");
+
+ // When the text is empty, and we show the hint text, there is a frame delay.
+ harness.run();
+ harness.snapshot("text_edit_scroll_1_5");
+}
+
#[test]
fn combobox_should_have_value() {
let harness = Harness::new_ui(|ui| {
@@ -118,3 +235,175 @@ fn interact_on_ui_response_should_be_stable() {
drop(harness);
assert_eq!(click_count, 10, "We missed some clicks!");
}
+
+fn has_red_warning_rect(output: &egui::FullOutput) -> bool {
+ output.shapes.iter().any(|clipped| {
+ matches!(
+ &clipped.shape,
+ Shape::Rect(rect_shape)
+ if rect_shape.stroke.color == Color32::RED
+ )
+ })
+}
+
+/// A button that changes its text on hover, with the Id derived from the text.
+/// This is a plausible bug: the widget keeps the same rect, but its Id changes
+/// between frames because the label (and thus the Id salt) changes on hover.
+/// The `warn_if_rect_changes_id` debug check should catch this.
+#[test]
+fn warn_if_rect_changes_id() {
+ let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0));
+
+ let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| {
+ // Simulate a buggy widget whose Id depends on its label text,
+ // and the label changes on hover:
+ let is_hovered = ui.rect_contains_pointer(button_rect);
+ let label = if is_hovered { "Hovering!" } else { "Click me" };
+ let id = ui.id().with(label);
+ let _response = ui.interact(button_rect, id, Sense::click());
+ });
+
+ // no hover — establishes stable prev_pass
+ harness.step();
+ assert!(
+ !has_red_warning_rect(harness.output()),
+ "Should not warn without hover"
+ );
+
+ // Move the pointer over the button
+ harness.hover_at(button_rect.center());
+
+ harness.step();
+ assert!(
+ has_red_warning_rect(harness.output()),
+ "Should warn when a widget rect changes Id between passes"
+ );
+}
+
+/// When a parent Ui's id changes (e.g. via `push_id` with a dynamic value),
+/// all child widget ids shift too. This should NOT trigger `warn_if_rect_changes_id` because the
+/// `parent_id` also changed — it's a cascading id shift, not a widget bug.
+#[test]
+fn warn_if_rect_changes_id_false_positive_parent_shift() {
+ use std::cell::Cell;
+
+ let counter = Cell::new(0);
+ let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0));
+
+ let mut harness = Harness::builder().with_size((200.0, 100.0)).build_ui(|ui| {
+ // push_id with a changing value causes the child Ui's id to shift,
+ // which in turn shifts all widget ids inside it.
+ ui.push_id(counter.get(), |ui| {
+ let id = ui.id().with("my_widget");
+ let _response = ui.interact(button_rect, id, Sense::click());
+ });
+ });
+
+ // Frame 1: counter=0 — establishes prev_pass
+ harness.step();
+ assert!(
+ !has_red_warning_rect(harness.output()),
+ "Should not warn on first frame"
+ );
+
+ // Frame 2: counter=0 — prev_pass == this_pass
+ harness.step();
+ assert!(
+ !has_red_warning_rect(harness.output()),
+ "Should not warn when nothing changed"
+ );
+
+ // Now change the parent id, shifting all child widget ids
+ counter.set(1);
+ harness.step();
+
+ assert!(
+ !has_red_warning_rect(harness.output()),
+ "Should NOT warn when parent Ui's id shifted (cascading id change)"
+ );
+}
+
+#[test]
+fn horizontal_wrapped_multiline_row_height() {
+ let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| {
+ ui.style_mut().interaction.tooltip_delay = 0.0;
+ ui.style_mut().interaction.show_tooltips_only_when_still = false;
+
+ let mut string = String::new();
+
+ ui.horizontal_wrapped(|ui| {
+ ui.monospace("| ");
+ let _ = ui.button("A");
+ let _ = ui.button("B");
+ ui.end_row();
+
+ ui.monospace("| ");
+ let _ = ui.button("C");
+ let _ = ui.button("D");
+ let _ = ui.button("E");
+ ui.end_row();
+
+ ui.monospace("| ");
+ ui.text_edit_multiline(&mut string);
+ ui.end_row();
+
+ ui.monospace("| ");
+ let _ = ui.button("F");
+ let _ = ui.button("G");
+ ui.end_row();
+
+ ui.monospace("| ");
+ let _ = ui.button("H");
+ let _ = ui.button("I");
+ let _ = ui.button("K");
+ ui.end_row();
+ });
+ });
+
+ harness.snapshot("horizontal_wrapped_multiline_row_height");
+}
+
+#[test]
+fn horizontal_wrapped_multiline_row_height_reference() {
+ let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| {
+ ui.style_mut().interaction.tooltip_delay = 0.0;
+ ui.style_mut().interaction.show_tooltips_only_when_still = false;
+
+ let mut string = String::new();
+
+ ui.vertical(|ui| {
+ ui.horizontal(|ui| {
+ ui.monospace("| ");
+ let _ = ui.button("A");
+ let _ = ui.button("B");
+ });
+
+ ui.horizontal(|ui| {
+ ui.monospace("| ");
+ let _ = ui.button("C");
+ let _ = ui.button("D");
+ let _ = ui.button("E");
+ });
+
+ ui.horizontal(|ui| {
+ ui.monospace("| ");
+ ui.text_edit_multiline(&mut string);
+ });
+
+ ui.horizontal(|ui| {
+ ui.monospace("| ");
+ let _ = ui.button("F");
+ let _ = ui.button("G");
+ });
+
+ ui.horizontal(|ui| {
+ ui.monospace("| ");
+ let _ = ui.button("H");
+ let _ = ui.button("I");
+ let _ = ui.button("K");
+ });
+ });
+ });
+
+ harness.snapshot("horizontal_wrapped_multiline_row_height_reference");
+}
diff --git a/tests/egui_tests/tests/snapshots/atom_letter_spacing.png b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png
new file mode 100644
index 000000000..89fba254e
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4aaf541ed0245777c802d31f01edb0cc4e53ebd2f4444e094336c180b98091d3
+size 2221
diff --git a/tests/egui_tests/tests/snapshots/grow_all.png b/tests/egui_tests/tests/snapshots/grow_all.png
index 3e5208fe0..89b96aba7 100644
--- a/tests/egui_tests/tests/snapshots/grow_all.png
+++ b/tests/egui_tests/tests/snapshots/grow_all.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2b91ae9e626d885b049d80dc9421275e147f4a3501c21ff4740b0f59d9c2998b
+oid sha256:83c3e19004462b793a5929f60f8b81a795c57529bfc74c6e87890aa4b9b8d939
size 13930
diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png
new file mode 100644
index 000000000..e6ac8e446
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef21b42f90401f6b85685e1cc37d07970b38d2b40394f53bbde5bd4f0d54fb95
+size 5340
diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png
new file mode 100644
index 000000000..a533a8401
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5025f4cb528ae5edc387149f1d14523ab4b93058f0862e775a1c2276a3e77af6
+size 5377
diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png
index 200ea6476..765e63f05 100644
--- a/tests/egui_tests/tests/snapshots/layout/atoms_image.png
+++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2e236f71e26e1a96acf9cd135b5db3a9cb0df374b87c3e283023dd14df193411
-size 369870
+oid sha256:24c85a987b0b80961b656f386f529b7538ddee59a030d02a0946d0f714ce7004
+size 368329
diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png
index 3c982b37e..3e37969f7 100644
--- a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png
+++ b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:096ec8246969f85cfa0cb8d58731be9aaf82b7dac70dc064ec999b1eed25e1ef
-size 368552
+oid sha256:97b26c9abaf655fa5ef0625b8bc61042291a8ea18ecc89ea16abd3be6368c006
+size 367314
diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png
index 664e23a9b..54a8a3e1c 100644
--- a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png
+++ b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0813583ca9658b5f27f3585e59f829b71c86061619d7f61a16cc2ccf0906a322
-size 291213
+oid sha256:47b09261afe84892cdb169cb99ae59c49f671e68b3e99fc170e304de9b2bf526
+size 290633
diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png
index 21449927d..635858aba 100644
--- a/tests/egui_tests/tests/snapshots/layout/button.png
+++ b/tests/egui_tests/tests/snapshots/layout/button.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e822c2324268d6e6168f9510aa1caec94df38dd0c163afcdecad11f2b1740936
-size 314449
+oid sha256:3cbc6f95073cbbb26729d287e5fe073c76e8bddee7eef95b431a873522234297
+size 313244
diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png
index 4ee6cffa2..6c63fb759 100644
--- a/tests/egui_tests/tests/snapshots/layout/button_image.png
+++ b/tests/egui_tests/tests/snapshots/layout/button_image.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:682dd89e15ee289a87a592c93ac2b9ec3172cd4fedcc02072c0516a9ae9ecd64
-size 335687
+oid sha256:8f14f770785d01b1673d1c8ca780bfff72e51992794dc7233cf5ec4ea99cb3e9
+size 350648
diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
index 5b74267e1..9c74cd8be 100644
--- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
+++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e2d22c9e7fd701be1dc1581635cdfa2829e02db9c6f66bf54eac106ebd7344a3
-size 421041
+oid sha256:231ceab75a602eedcd11f4f4ed34f38fb9d072f5cb54e135a7e02d33d257f86b
+size 433973
diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox.png b/tests/egui_tests/tests/snapshots/layout/checkbox.png
index c1e993885..766aedaca 100644
--- a/tests/egui_tests/tests/snapshots/layout/checkbox.png
+++ b/tests/egui_tests/tests/snapshots/layout/checkbox.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ee91ad31d625930c55ae4ac41011f2018ef11ba20cefe5686b7338671fd6c32e
-size 389522
+oid sha256:2f17fe1f7b2cccaa8991559218a7f13f61e459dc8443cf0fe2d24df7e9bd2eea
+size 388959
diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
index 4b972d966..8f79ec659 100644
--- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
+++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bcb5e0ec12a4bb7aba8ca8b53622fb2c204411ec66d7745bdb06e01bd1ffc731
-size 417596
+oid sha256:552a4d4933768ea1ee2323e7946f74f9ddd7e2f7b7c6d9f94bb92c8e7dd230a4
+size 416630
diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png
index 44bf0bfcb..bfe289a61 100644
--- a/tests/egui_tests/tests/snapshots/layout/drag_value.png
+++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b2cd4d27748e193d4f46ad7a5be6ff411ad3152b4fd546c0dc98dd3bb5333d93
-size 236090
+oid sha256:339772a7974a2136b222697af2dd6e0202295d78e0720645204feb3c291481af
+size 263181
diff --git a/tests/egui_tests/tests/snapshots/layout/radio.png b/tests/egui_tests/tests/snapshots/layout/radio.png
index d3930768e..2fbd917a8 100644
--- a/tests/egui_tests/tests/snapshots/layout/radio.png
+++ b/tests/egui_tests/tests/snapshots/layout/radio.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c15ece11f5c45d4bb89096a4d7146032e109fd9a099f2f37641e2676f7c3e184
-size 327971
+oid sha256:275c5358d3cfcbae7dfbeae4eac6606e2f394023837da492adc85934a972203e
+size 325936
diff --git a/tests/egui_tests/tests/snapshots/layout/radio_checked.png b/tests/egui_tests/tests/snapshots/layout/radio_checked.png
index c2d12eb98..e95932c0d 100644
--- a/tests/egui_tests/tests/snapshots/layout/radio_checked.png
+++ b/tests/egui_tests/tests/snapshots/layout/radio_checked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5942409a24177f84e067bcb488d8f976a0a6ad432f9f8603be2fdd4269d79efa
-size 347946
+oid sha256:8bde6a904873ec2ffd7a194b820f3d76db5cacb3c266f3cb99f1c77ca2bd69fb
+size 346473
diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value.png b/tests/egui_tests/tests/snapshots/layout/selectable_value.png
index e2ea0c1f4..fd2daeeb0 100644
--- a/tests/egui_tests/tests/snapshots/layout/selectable_value.png
+++ b/tests/egui_tests/tests/snapshots/layout/selectable_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2c082417d4f65be1efc6c040d2acaf02d899ceaa547ba86f530e1d2e94f4e385
-size 389160
+oid sha256:e0b87d78fce32144f1c694beb637461cb70b9127346c90d0276a877db0700291
+size 387935
diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
index 2a2553a30..8ce768dae 100644
--- a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
+++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7edb1db196e1a6c740503d976f5f8e4dd9d3d4dd07e8391ce77f01f411cae315
-size 402030
+oid sha256:77d4dd1a05771c25af933398d4f118e5e21a31b2e4db66161cf054fb1d7ebe24
+size 400911
diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png
index b7d9edcd5..83da462b7 100644
--- a/tests/egui_tests/tests/snapshots/layout/slider.png
+++ b/tests/egui_tests/tests/snapshots/layout/slider.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e8bd1515d5c4045f4cd1b5d0c4f48469bd7e3ce738a95f741e9254e02ea28185
-size 276004
+oid sha256:b4071301c08f980ee26d914e4a4724b3f46f1113c62495483d9b0df980d8cbcd
+size 274770
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png
index 379b33806..d5d853f5e 100644
--- a/tests/egui_tests/tests/snapshots/layout/text_edit.png
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:61dde59ee92a1c22aba7fd8decf62d88d1ed81c10cd969ce65c451185f7ca58b
-size 221618
+oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3
+size 220588
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png
index ccc29355f..d87f37561 100644
--- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c2a7ad1a4568f0ed7f203453697982603fad8b7e9852b4193216ebff1624671d
-size 384210
+oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7
+size 372041
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
index 9ac2cefee..e65f04b1c 100644
--- a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:54a2f4004a71af18ffc42bba723a69855af4913ddedd8185688a59f9967e5a13
-size 509495
+oid sha256:aed677ddda9544258ddc58ed602655f6a62ab2d1d8342accd025593bbcb25e2f
+size 506926
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png
index e74e0f928..578e4d9db 100644
--- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2ab3a86f34c5cce033903cd67c1070dcc509e385e62e05358e1329968bfb1e95
-size 363693
+oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc
+size 330200
diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png
new file mode 100644
index 000000000..bdcab38f2
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179
+size 361876
diff --git a/tests/egui_tests/tests/snapshots/max_width.png b/tests/egui_tests/tests/snapshots/max_width.png
index 6534961a8..be50f81db 100644
--- a/tests/egui_tests/tests/snapshots/max_width.png
+++ b/tests/egui_tests/tests/snapshots/max_width.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea5546e2e72aa5181edfe260cf5b506a30fea8c3db049c080bafc303223ba95f
+oid sha256:df3c1ba38afa30d22106d21a54621c28a0de2b98f77f4d7e398f09089286ef3e
size 8367
diff --git a/tests/egui_tests/tests/snapshots/max_width_and_grow.png b/tests/egui_tests/tests/snapshots/max_width_and_grow.png
index 54dddf7e8..d49489c41 100644
--- a/tests/egui_tests/tests/snapshots/max_width_and_grow.png
+++ b/tests/egui_tests/tests/snapshots/max_width_and_grow.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7d65a6c7e855a5476369422577d02f5e2a96814b100d7385f172fa9506189849
+oid sha256:f5919c35a3d736e0c432b3a94d6ab2a2f936f71852b94f2f95475fa6ab5281ad
size 8369
diff --git a/tests/egui_tests/tests/snapshots/rotated_ellipse.png b/tests/egui_tests/tests/snapshots/rotated_ellipse.png
new file mode 100644
index 000000000..e32f7864c
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/rotated_ellipse.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8f222733524b21969834a9ccc15aa9b0a4deb1d41e1086c80750f7cdd9711c8
+size 17324
diff --git a/tests/egui_tests/tests/snapshots/rotated_rect.png b/tests/egui_tests/tests/snapshots/rotated_rect.png
new file mode 100644
index 000000000..52255aa7f
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/rotated_rect.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb4f1d10aa664e04da4b2e38c52cb6516a4c43a98884c9223e15266ea28ccd3d
+size 14191
diff --git a/tests/egui_tests/tests/snapshots/shrink_first_text.png b/tests/egui_tests/tests/snapshots/shrink_first_text.png
index 81680a36a..a623d8b3b 100644
--- a/tests/egui_tests/tests/snapshots/shrink_first_text.png
+++ b/tests/egui_tests/tests/snapshots/shrink_first_text.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77ff29a1441d11f3b13ddaf5f6dd5f2c5781bc418887e1c2eabe00679958cba6
+oid sha256:73b1cc01da110554dd41f4e5134f5d6d34b7e2079d5ac776f40980d616481ffc
size 11448
diff --git a/tests/egui_tests/tests/snapshots/shrink_last_text.png b/tests/egui_tests/tests/snapshots/shrink_last_text.png
index 6f7b28c16..cda1a2add 100644
--- a/tests/egui_tests/tests/snapshots/shrink_last_text.png
+++ b/tests/egui_tests/tests/snapshots/shrink_last_text.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:23923d37e4dd848b043c7118e651ddade82c0df180652d8f0dcb829b1b6245d6
-size 12009
+oid sha256:00e129a40ea9815472ab9d823a1801fbdd268bd58745cad1c1c3dd91309c61fc
+size 12010
diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png
index 842f41171..534b55d92 100644
--- a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040
-size 8802
+oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9
+size 8833
diff --git a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png
index 099d55cb5..773b93629 100644
--- a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43
-size 1242
+oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c
+size 1273
diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png
index 842f41171..534b55d92 100644
--- a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040
-size 8802
+oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9
+size 8833
diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png
index 099d55cb5..773b93629 100644
--- a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43
-size 1242
+oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c
+size 1273
diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png
index 842f41171..534b55d92 100644
--- a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040
-size 8802
+oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9
+size 8833
diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png
index 099d55cb5..773b93629 100644
--- a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43
-size 1242
+oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c
+size 1273
diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png
index 842f41171..534b55d92 100644
--- a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040
-size 8802
+oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9
+size 8833
diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png
index 099d55cb5..773b93629 100644
--- a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43
-size 1242
+oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c
+size 1273
diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png
index 842f41171..534b55d92 100644
--- a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3afbf9e4d598907f088d3f09b1cf2b70c682062f1f4b98aa98b997121f763040
-size 8802
+oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9
+size 8833
diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png
index 099d55cb5..773b93629 100644
--- a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png
+++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a0a38c58ae7a30256e9491bfeb1155f2df6bba2a656ed9611fa945cbe2ebdc43
-size 1242
+oid sha256:8284e4828d60a4d65e848d08a95f6fbf681b8632877e9e2961fdb7e4876ffe2c
+size 1273
diff --git a/tests/egui_tests/tests/snapshots/size_max_size.png b/tests/egui_tests/tests/snapshots/size_max_size.png
index 12b526287..499259fd4 100644
--- a/tests/egui_tests/tests/snapshots/size_max_size.png
+++ b/tests/egui_tests/tests/snapshots/size_max_size.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f2d9b0884adb89f598dd0c7eb421c0c8e8bcdaa1cbca02f4646c777711a005c2
+oid sha256:11a987f7376f8a3174958a8c21bece8bfb7ec284077940d87038271717d2c397
size 8655
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png
new file mode 100644
index 000000000..58b0b13f2
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e
+size 2262
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png
new file mode 100644
index 000000000..c1920bcf1
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702
+size 2272
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png
new file mode 100644
index 000000000..bc1ffcd08
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a
+size 714
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png
new file mode 100644
index 000000000..324946e10
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
+size 775
diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png
new file mode 100644
index 000000000..324946e10
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
+size 775
diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png
new file mode 100644
index 000000000..29546a036
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:502607c803b884e4e1640d39c97b03b0a40df93c2da328f889168e386f837f36
+size 13261
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
index 440e1e818..796a1e3b5 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddd940c1ed581471b1448c04296d48b829939025980479a122edfe9b9bd0321e
-size 2321
+oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa
+size 2296
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
index d0f6cb316..cd6d5a621 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a53c074ad4cb1f0e38d6d7144cb661a2b68d809203bfca636ff5a60d8582a651
-size 2288
+oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
+size 2982
diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
index e618bf8dd..cd6d5a621 100644
--- a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
+++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0a9be0d364374237ea9c3cbfc3703f47f4345d81cecbdcf6c4b49688c4c282ad
+oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
size 2982
diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png
new file mode 100644
index 000000000..1d0a5ed46
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a
+size 1510
diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png
new file mode 100644
index 000000000..bdb8d1b1b
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea
+size 1501
diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png
index ffe31f06a..e6978c70f 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec81a46d44402d5e709d825e42de99a2f7b9707f77dc5f94e277ae9fd77b6fae
-size 10903
+oid sha256:9ed487544a84f9f128af550030bc7fe8a960bc70897b38f7c858440a42b6ce44
+size 11197
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png
index 162712f09..6cb7241bf 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a6a3eb59ff208d106c3808265d4bef10d80b634f74d99476c3541d997b30bc56
-size 11967
+oid sha256:d53f67fb3a3717f7bc5ce99b93bc21d1d6580899dfe8e1371ff22bb416af0786
+size 12114
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
index 4027ef68b..b278f6c25 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:176df42437cc8f6f97bba4b0f9bea72c9359f69e66abfa289d0701814f8ad258
-size 14746
+oid sha256:e298d89e6fb434e5010d96661fca40bf119118b6b31fdd9fc13201bcd74c8ffd
+size 15149
diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
index b742787c0..9a1e15c20 100644
--- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
+++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5003e67baed533e485448b953b616804047b9da25d2758b288c96e65d7f37b0f
-size 14323
+oid sha256:a0581d601f1e536298cb52bfc8a167aa37aebdf065fc910973a752c9c159223d
+size 14733
diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png
index 2b04df5a5..7cd2d2ce8 100644
--- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png
+++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3d42e002c3fd34f96d58ddfd4d2f91cf1ac7755ff71b5da315be4bee6bf00e03
-size 8411
+oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a
+size 9079
diff --git a/tests/egui_tests/tests/snapshots/visuals/radio.png b/tests/egui_tests/tests/snapshots/visuals/radio.png
index 4da91f2b3..b35eb2d51 100644
--- a/tests/egui_tests/tests/snapshots/visuals/radio.png
+++ b/tests/egui_tests/tests/snapshots/visuals/radio.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d54388fca5de500aafde4efc73df5cc15a72ce5443c9f527ffb70430c08e60e9
-size 11871
+oid sha256:4502cc58a4085d1e0f9945d0bd1d25adeefe71094ce94a210c57f113727f3a5a
+size 11806
diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png
index f91c4a549..7bdae8cf1 100644
--- a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png
+++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8aec08c983f71cf4fa81e88ba1751de1253d9ba6d28692b824912ad4764471bc
-size 12563
+oid sha256:cdedec6788b1a5026603322db9dd9f5fa398813d8aa2c56bc60acad390110501
+size 12499
diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
index 88069ed9d..2f3192a74 100644
--- a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
+++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:79f8fd0df269a45ee5a8cb6ddd1501a562e92de2bec15ab28016ceb2834c3c90
-size 13908
+oid sha256:23d1cddf87ea10d6735403ea0b2a16811d4f92246415633d393c991c3bfab2a1
+size 13716
diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
index 2ab268abb..66f3df875 100644
--- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
+++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:966c55de5786c1ad0165e0cca29481d51770e23173b5dda14e40013669d3db6d
-size 13889
+oid sha256:e8ac8bbdf9331dbe4244aa2964adf9f49ab8981b899aee9f3200b2799cdf7bc0
+size 13731
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png
index 4de5e3bd0..4719c8ce9 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e
-size 8159
+oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0
+size 8127
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
index 192fa8f74..8a5999742 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79
-size 10959
+oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd
+size 10143
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
index dbfa8856a..f02b65693 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b1533e0ebaf0bd161e651d21ed81d36e8f0be06003357d9e5091ce2db4df5b7d
-size 21517
+oid sha256:f6cf861a5c1682add50f9bdee4672e5fcaf882329566097faecab5312ac509b7
+size 21419
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
index decd09bf9..19c231b45 100644
--- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3
-size 10302
+oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae
+size 8838
diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png
new file mode 100644
index 000000000..d27f6f8c4
--- /dev/null
+++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc
+size 10028
diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs
index 6f4b694e6..f7e0a4af1 100644
--- a/tests/egui_tests/tests/test_atoms.rs
+++ b/tests/egui_tests/tests/test_atoms.rs
@@ -92,17 +92,17 @@ fn test_intrinsic_size() {
if let Some(current_intrinsic_size) = intrinsic_size {
assert_eq!(
Some(current_intrinsic_size),
- response.intrinsic_size,
+ response.intrinsic_size(),
"For wrapping: {wrapping:?}"
);
}
assert!(
- response.intrinsic_size.is_some(),
+ response.intrinsic_size().is_some(),
"intrinsic_size should be set for `Button`"
);
- intrinsic_size = response.intrinsic_size;
+ intrinsic_size = response.intrinsic_size();
if wrapping == TextWrapMode::Extend {
- assert_eq!(Some(response.rect.size()), response.intrinsic_size);
+ assert_eq!(Some(response.rect.size()), response.intrinsic_size());
}
});
}
@@ -119,3 +119,24 @@ fn test_button_shortcut_text() {
harness.snapshot("button_shortcut");
}
+
+/// Tests the spacing between galleys.
+/// All of these should look the same.
+#[test]
+fn test_atom_letter_spacing() {
+ use egui::AtomLayout;
+
+ let mut harness = HarnessBuilder::default().build_ui(|ui| {
+ ui.add(AtomLayout::new("1.00x").gap(0.0));
+ ui.add(AtomLayout::new(("1.00", "x")).gap(0.0));
+ ui.horizontal(|ui| {
+ ui.spacing_mut().item_spacing.x = 0.0;
+ ui.label("1.00");
+ ui.label("x");
+ });
+ });
+ harness.run();
+ harness.fit_contents();
+
+ harness.snapshot("atom_letter_spacing");
+}
diff --git a/tests/egui_tests/tests/test_rotation.rs b/tests/egui_tests/tests/test_rotation.rs
new file mode 100644
index 000000000..03c9a8664
--- /dev/null
+++ b/tests/egui_tests/tests/test_rotation.rs
@@ -0,0 +1,117 @@
+use egui::epaint::{EllipseShape, RectShape, StrokeKind};
+use egui::{Color32, Grid, Pos2, Rect, Shape, Stroke, Vec2};
+use egui_kittest::Harness;
+
+const SHAPE_COLOR: Color32 = Color32::from_rgb(255, 165, 0);
+const GHOST_COLOR: Color32 = Color32::from_rgb(0, 255, 255);
+const PIVOT_COLOR: Color32 = Color32::from_rgb(255, 0, 255);
+const CELL_SIZE: Vec2 = Vec2::new(180.0, 180.0);
+
+#[test]
+fn rotated_rect() {
+ let shape_stroke = Stroke::new(2.0, Color32::BLACK);
+ let ghost_stroke = Stroke::new(1.0, GHOST_COLOR);
+
+ let mut harness = Harness::new_ui(|ui| {
+ ui.ctx().set_pixels_per_point(1.0);
+
+ let rect_size = Vec2::new(100.0, 60.0);
+ let cell_center = Pos2::new(90.0, 90.0);
+ let cell_rect = Rect::from_center_size(cell_center, rect_size);
+
+ Grid::new("rotated_rect_grid")
+ .spacing(Vec2::new(30.0, 30.0))
+ .show(ui, |ui| {
+ for (label, angle, pivot) in [
+ ("0°", 0.0, None),
+ ("Center 45°", 45.0f32.to_radians(), None),
+ (
+ "Top-Left 45°",
+ 45.0f32.to_radians(),
+ Some(cell_rect.left_top()),
+ ),
+ ] {
+ paint_case(ui, label, |offset| {
+ let rect = cell_rect.translate(offset);
+ let pivot = pivot.map(|p| p + offset);
+ let pivot_pos = pivot.unwrap_or_else(|| rect.center());
+
+ let ghost = RectShape::stroke(rect, 0.0, ghost_stroke, StrokeKind::Outside);
+ let shape = RectShape::new(
+ rect,
+ 0.0,
+ SHAPE_COLOR,
+ shape_stroke,
+ StrokeKind::Outside,
+ )
+ .with_angle_and_pivot(angle, pivot_pos);
+
+ (ghost.into(), shape.into(), pivot_pos)
+ });
+ }
+ });
+ });
+
+ harness.fit_contents();
+ harness.try_snapshot("rotated_rect").unwrap();
+}
+
+#[test]
+fn rotated_ellipse() {
+ let shape_stroke = Stroke::new(2.0, Color32::BLACK);
+ let ghost_stroke = Stroke::new(1.0, GHOST_COLOR);
+
+ let mut harness = Harness::new_ui(|ui| {
+ ui.ctx().set_pixels_per_point(1.0);
+
+ let rect_size = Vec2::new(100.0, 60.0);
+ let cell_center = Pos2::new(90.0, 90.0);
+ let radius = rect_size / 2.0;
+
+ Grid::new("rotated_ellipse_grid")
+ .spacing(Vec2::new(30.0, 30.0))
+ .show(ui, |ui| {
+ for (label, angle, pivot) in [
+ ("0°", 0.0, None),
+ ("Center 45°", 45.0f32.to_radians(), None),
+ (
+ "Top-Left 45°",
+ 45.0f32.to_radians(),
+ Some(cell_center - radius),
+ ),
+ ] {
+ paint_case(ui, label, |offset| {
+ let center = cell_center + offset;
+ let pivot = pivot.map(|p| p + offset);
+ let pivot_pos = pivot.unwrap_or(center);
+
+ let ghost = EllipseShape::stroke(center, radius, ghost_stroke);
+ let mut shape = EllipseShape::filled(center, radius, SHAPE_COLOR);
+ shape.stroke = shape_stroke;
+ let shape = shape.with_angle_and_pivot(angle, pivot_pos);
+
+ (ghost.into(), shape.into(), pivot_pos)
+ });
+ }
+ });
+ });
+
+ harness.fit_contents();
+ harness.try_snapshot("rotated_ellipse").unwrap();
+}
+
+fn paint_case(ui: &mut egui::Ui, label: &str, make_shapes: F)
+where
+ F: FnOnce(Vec2) -> (Shape, Shape, Pos2),
+{
+ ui.vertical(|ui| {
+ ui.label(label);
+ let (response, painter) = ui.allocate_painter(CELL_SIZE, egui::Sense::hover());
+ let offset = response.rect.min.to_vec2();
+
+ let (ghost, shape, pivot) = make_shapes(offset);
+ painter.add(ghost);
+ painter.add(shape);
+ painter.circle_filled(pivot, 3.0, PIVOT_COLOR);
+ });
+}
diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs
index a53ad4a0c..5283b21d4 100644
--- a/tests/egui_tests/tests/test_widgets.rs
+++ b/tests/egui_tests/tests/test_widgets.rs
@@ -4,8 +4,9 @@ use egui::accesskit::Role;
use egui::load::SizedTexture;
use egui::{
Align, AtomExt as _, AtomLayout, Button, Color32, ColorImage, Direction, DragValue, Event,
- Grid, IntoAtoms as _, Layout, PointerButton, Response, Slider, Stroke, StrokeKind, TextEdit,
- TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _, include_image,
+ Grid, IntoAtoms as _, Layout, PointerButton, Response, RichText, Slider, Stroke, StrokeKind,
+ TextEdit, TextWrapMode, TextureHandle, TextureOptions, Ui, UiBuilder, Vec2, Widget as _,
+ include_image,
};
use egui_kittest::kittest::{Queryable as _, by};
use egui_kittest::{Harness, Node, SnapshotResult, SnapshotResults};
@@ -74,7 +75,11 @@ fn widget_tests() {
test_widget(
"drag_value",
- |ui| DragValue::new(&mut 12.0).ui(ui),
+ |ui| {
+ DragValue::new(&mut 12.0)
+ .suffix(RichText::new(" px").weak().small())
+ .ui(ui)
+ },
&mut results,
);
@@ -117,6 +122,18 @@ fn widget_tests() {
},
&mut results,
);
+ test_widget(
+ "text_edit_prefix_suffix",
+ |ui| {
+ ui.spacing_mut().text_edit_width = 45.0;
+ TextEdit::singleline(&mut "Hello World".to_owned())
+ .prefix("🔎")
+ .suffix("!")
+ .clip_text(true)
+ .ui(ui)
+ },
+ &mut results,
+ );
test_widget(
"slider",
diff --git a/tests/test_background_logic/Cargo.toml b/tests/test_background_logic/Cargo.toml
new file mode 100644
index 000000000..92985d5ee
--- /dev/null
+++ b/tests/test_background_logic/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "test_background_logic"
+version = "0.1.0"
+license = "MIT OR Apache-2.0"
+edition = "2024"
+rust-version = "1.92"
+publish = false
+
+[lints]
+workspace = true
+
+[dependencies]
+eframe = { workspace = true, features = ["default"] }
+env_logger = { workspace = true, features = ["auto-color", "humantime"] }
diff --git a/tests/test_background_logic/src/main.rs b/tests/test_background_logic/src/main.rs
new file mode 100644
index 000000000..ea80cfa9e
--- /dev/null
+++ b/tests/test_background_logic/src/main.rs
@@ -0,0 +1,64 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+#![expect(rustdoc::missing_crate_level_docs)]
+#![allow(clippy::print_stderr)]
+
+use std::time::Duration;
+
+use eframe::egui::{self, ViewportInfo};
+
+fn main() {
+ env_logger::init();
+
+ let _ = eframe::run_native(
+ "Background Logic Test",
+ eframe::NativeOptions {
+ viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 200.0]),
+ ..Default::default()
+ },
+ Box::new(|_cc| Ok(Box::new(App))),
+ );
+}
+
+struct App;
+
+impl eframe::App for App {
+ fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
+ eprintln!("App::logic called {}", viewport_info(ctx));
+ ctx.request_repaint_after(Duration::from_secs(1));
+ }
+
+ fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
+ eprintln!("App::ui called {}", viewport_info(ui.ctx()));
+ ui.centered_and_justified(|ui| {
+ ui.heading("Minimize this window");
+ });
+ }
+}
+
+fn viewport_info(ctx: &egui::Context) -> String {
+ ctx.input(|i| {
+ let ViewportInfo {
+ minimized,
+ focused,
+ occluded,
+ ..
+ } = i.viewport();
+
+ let visible = i.viewport().visible();
+
+ let mut s = String::new();
+
+ let flags = [
+ ("focused", focused),
+ ("occluded", occluded),
+ ("minimized", minimized),
+ ("visible", &visible),
+ ];
+ for (name, value) in flags {
+ if let Some(value) = value {
+ s += &format!(" {name}={value}");
+ }
+ }
+ s
+ })
+}
diff --git a/web_demo/favicon.ico b/web_demo/favicon.ico
old mode 100755
new mode 100644
index 61ad031aa..59a822c61
Binary files a/web_demo/favicon.ico and b/web_demo/favicon.ico differ