Update egui/eframe to 0.37 (#9933)

* Update egui/eframe to 0.37 (WIP)

egui, eframe and egui-wgpu 0.36 -> 0.37. SolidCraft's egui-wgpu patch is
rebased onto 0.37.0; the vendored accesskit_macos is dropped (0.27.2 has
the #7535 fix).

Not finished: 36 solidcraft-ui-egui lib tests fail, and the full lib run
crashes with SIGSEGV when tests run in parallel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: clear texture deltas that egui 0.37 refuses to drop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop non-finite pointer input before egui; keep test time monotonic

egui 0.37 debug-asserts on NaN wheel deltas, on time moving backwards in the
pointer history, and on add_space inside a Grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hold a click that follows typing in the same frame until the next frame

egui 0.37 drops a field's focus on a click elsewhere when the field first
reads its style, before it reads the frame's typing, so text typed in the
frame of a click was lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name a document tab's close button "Close tab"

egui 0.37 names an unnamed widget after its tooltip, so the tab's × became a
second control named Close beside a dialog's. The space mouse guard test now
clicks where each button was: by name it found nothing behind the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Turn on egui's default_fonts: 0.37 no longer does

The built-in Hack is the UI's symbol fallback (#2955). The app got it through
eframe, but solidcraft-ui-egui on its own (its tests) did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Text menu: act on the selection the menu click took the focus from

egui 0.37 drops a field's focus on a click elsewhere before the field draws,
and an unfocused field loses its selection, so Copy, Cut and Paste from the
right-click menu saw no selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Placement field: keep its native height under egui 0.37's interact floor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: pin the theme's 2/3 point radii; measure Japanese ribbon labels with a Japanese face

egui 0.37 rounds widgets 4 points by default, which the theme deliberately
does not follow. The ribbon test measured Japanese with missing glyphs, whose
width changed in 0.37; with BIZ UDPGothic, 駆動 wraps after its particle like
変換 does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: click the render inspector's Perspective checkbox by position

egui 0.37 orders the accessibility tree differently, so the name found the
stage's Perspective segment first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Translate "Close tab" for the complete catalogs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Point the focus workarounds at emilk/egui#8763

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cargo.lock: update only what egui 0.37 needs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web: Id::new -> Id::unique for the wasm-only windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Command search: keep its hint centred under egui 0.37's interact floor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* egui 0.37 renames in code main added since the branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* egui 0.37 renames in code main added since the last rebase

Id::new to Id::unique, ui.id() to ui.scope_id(), WidgetType to Role, the
wheel events' source, and the button stroke read from the widget visuals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Review: disclosures report expanded, late screenshots drained, chat width reset on both sides

A disclosure's node says whether it is open as expanded, as egui's own
collapsing header does. The screenshot channel is drained every frame, so a
shot whose request was cancelled is dropped. Resetting the chat panel's width
forgets both sides' state, as before 0.37, and the request stays with its
window when tabs switch. Tests main added since the last rebase clear their
texture deltas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JIJINBEIandClaude Opus 5.5 authored and GitHub committed 2026-11-05 10:31:50 +00:00
1 parent ad230eb4a4
commit 16f3b939b3
234 files changed
+2094 -4762

No files matched your search

+1 -2
View File
@@ -896,11 +896,10 @@ copied into SolidCraft.
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
## Vendored renderer compatibility fix
`vendor/egui-wgpu` is egui-wgpu 0.36.2 from https://github.com/emilk/egui
`vendor/egui-wgpu` is egui-wgpu 0.37.0 from https://github.com/emilk/egui
(MIT OR Apache-2.0; both original licence texts are included). Rust sources and
its original `egui.wgsl` shader are retained; no images or other artwork are copied.
SolidCraft's narrow patch selects a safe 1x MSAA path on the chosen OpenGL adapter
Generated
+168 -163
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "accesskit"
version = "0.24.1"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be"
dependencies = [
"enumn",
"serde",
@@ -15,12 +15,12 @@ dependencies = [
[[package]]
name = "accesskit_atspi_common"
version = "0.18.1"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954"
checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d"
dependencies = [
"accesskit",
"accesskit_consumer 0.36.0",
"accesskit_consumer",
"atspi-common",
"phf",
"serde",
@@ -29,41 +29,37 @@ dependencies = [
[[package]]
name = "accesskit_consumer"
version = "0.35.0"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
]
[[package]]
name = "accesskit_consumer"
version = "0.36.0"
name = "accesskit_ios"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60"
checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_consumer"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
"accesskit_consumer",
"hashbrown 0.17.1",
"objc2 0.5.3",
"objc2-foundation 0.2.2",
"objc2-ui-kit 0.2.2",
]
[[package]]
name = "accesskit_macos"
version = "0.26.3"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84"
dependencies = [
"accesskit",
"accesskit_consumer 0.38.0",
"hashbrown 0.16.1",
"accesskit_consumer",
"hashbrown 0.17.1",
"objc2 0.5.3",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -71,9 +67,9 @@ dependencies = [
[[package]]
name = "accesskit_unix"
version = "0.21.1"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c"
checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -89,13 +85,13 @@ dependencies = [
[[package]]
name = "accesskit_windows"
version = "0.32.1"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc"
dependencies = [
"accesskit",
"accesskit_consumer 0.35.0",
"hashbrown 0.16.1",
"accesskit_consumer",
"hashbrown 0.17.1",
"static_assertions",
"windows",
"windows-core",
@@ -103,11 +99,12 @@ dependencies = [
[[package]]
name = "accesskit_winit"
version = "0.32.2"
version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc"
dependencies = [
"accesskit",
"accesskit_ios",
"accesskit_macos",
"accesskit_unix",
"accesskit_windows",
@@ -203,6 +200,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -687,42 +685,18 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "calloop"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
dependencies = [
"bitflags 2.13.2",
"polling",
"rustix 1.1.5",
"tracing",
]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop 0.13.0",
"calloop",
"rustix 0.38.44",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "calloop-wayland-source"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
dependencies = [
"calloop 0.14.5",
"rustix 1.1.5",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.1.2"
@@ -1123,9 +1097,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecolor"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2"
dependencies = [
"bytemuck",
"emath",
@@ -1134,9 +1108,9 @@ dependencies = [
[[package]]
name = "eframe"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2"
checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84"
dependencies = [
"ahash",
"bytemuck",
@@ -1159,6 +1133,7 @@ dependencies = [
"profiling",
"raw-window-handle",
"ron",
"send_wrapper",
"serde",
"static_assertions",
"wasm-bindgen",
@@ -1171,9 +1146,9 @@ dependencies = [
[[package]]
name = "egui"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f"
dependencies = [
"accesskit",
"ahash",
@@ -1193,7 +1168,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.36.2"
version = "0.37.0"
dependencies = [
"ahash",
"bytemuck",
@@ -1211,32 +1186,34 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc"
checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de"
dependencies = [
"accesskit_winit",
"arboard",
"block2 0.6.2",
"bytemuck",
"egui",
"log",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"objc2-ui-kit 0.3.2",
"profiling",
"raw-window-handle",
"serde",
"smithay-clipboard",
"web-time",
"webbrowser",
"windows-sys 0.61.2",
"winit",
]
[[package]]
name = "egui_glow"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b"
checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268"
dependencies = [
"bytemuck",
"egui",
@@ -1254,9 +1231,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "emath"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5"
dependencies = [
"bytemuck",
"serde",
@@ -1330,9 +1307,9 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3"
checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd"
dependencies = [
"ahash",
"bytemuck",
@@ -1350,15 +1327,16 @@ dependencies = [
"skrifa",
"smallvec",
"unicode-general-category",
"unicode-properties",
"unicode-segmentation",
"vello_cpu",
]
[[package]]
name = "epaint_default_fonts"
version = "0.36.2"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19"
[[package]]
name = "equivalent"
@@ -1444,6 +1422,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.10"
@@ -1455,6 +1439,12 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1651,12 +1641,12 @@ dependencies = [
[[package]]
name = "glifo"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a"
checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be"
dependencies = [
"bytemuck",
"foldhash",
"foldhash 0.2.0",
"hashbrown 0.17.1",
"log",
"peniko",
@@ -1826,6 +1816,15 @@ dependencies = [
"smallvec",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1834,7 +1833,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"foldhash 0.2.0",
]
[[package]]
@@ -1843,7 +1842,7 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash",
"foldhash 0.2.0",
]
[[package]]
@@ -2500,6 +2499,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -2956,6 +2964,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -3005,10 +3023,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "phf"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
dependencies = [
"phf_macros",
"phf_shared",
@@ -3017,9 +3046,9 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
dependencies = [
"fastrand",
"phf_shared",
@@ -3027,9 +3056,9 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
dependencies = [
"phf_generator",
"phf_shared",
@@ -3040,9 +3069,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
dependencies = [
"siphasher",
]
@@ -3762,6 +3791,12 @@ version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[package]]
name = "serde"
version = "1.0.229"
@@ -3986,8 +4021,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.13.2",
"calloop 0.13.0",
"calloop-wayland-source 0.3.0",
"calloop",
"calloop-wayland-source",
"cursor-icon",
"libc",
"log",
@@ -4004,44 +4039,6 @@ dependencies = [
"xkeysym",
]
[[package]]
name = "smithay-client-toolkit"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
"bitflags 2.13.2",
"calloop 0.14.5",
"calloop-wayland-source 0.4.1",
"cursor-icon",
"libc",
"log",
"memmap2",
"rustix 1.1.5",
"thiserror 2.0.21",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-experimental",
"wayland-protocols-misc",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]]
name = "smithay-clipboard"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
dependencies = [
"libc",
"smithay-client-toolkit 0.20.0",
"wayland-backend",
]
[[package]]
name = "smol_str"
version = "0.2.2"
@@ -4663,7 +4660,6 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -4689,6 +4685,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -4763,6 +4770,12 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
@@ -4846,9 +4859,9 @@ dependencies = [
[[package]]
name = "vello_common"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd"
dependencies = [
"bytemuck",
"fearless_simd",
@@ -4861,9 +4874,9 @@ dependencies = [
[[package]]
name = "vello_cpu"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea"
checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9"
dependencies = [
"bytemuck",
"glifo",
@@ -5038,32 +5051,6 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-experimental"
version = "20250721.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-misc"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.12"
@@ -5641,7 +5628,7 @@ dependencies = [
"bitflags 2.13.2",
"block2 0.5.1",
"bytemuck",
"calloop 0.13.0",
"calloop",
"cfg_aliases",
"concurrent-queue",
"core-foundation 0.9.4",
@@ -5662,7 +5649,7 @@ dependencies = [
"raw-window-handle",
"redox_syscall 0.4.1",
"rustix 0.38.44",
"smithay-client-toolkit 0.19.2",
"smithay-client-toolkit",
"smol_str",
"tracing",
"unicode-segmentation",
@@ -5705,6 +5692,24 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 1.1.5",
"thiserror 2.0.21",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.4"
+5 -7
View File
@@ -103,11 +103,12 @@ tiny-skia = { version = "0.12", default-features = false, features = ["std", "si
toml = "1"
# ui
egui = "0.36"
eframe = { version = "0.36", default-features = false, features = ["persistence"] }
# default_fonts: the built-in Hack is the UI's symbol fallback (#2955); 0.37 no longer turns it on.
egui = { version = "0.37", features = ["default_fonts"] }
eframe = { version = "0.37", default-features = false, features = ["persistence"] }
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
web-time = "1"
egui-wgpu = "0.36"
egui-wgpu = "0.37"
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
naga = { version = "30", default-features = false }
@@ -162,12 +163,9 @@ debug = "line-tables-only"
egui-wgpu = { path = "vendor/egui-wgpu" }
# Deliver trackpad pinch and rotation gestures on Wayland (zwp_pointer_gestures_v1), which
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
# winit 0.30 never binds and eframe 0.37 still requires winit 0.30.
winit = { path = "vendor/winit" }
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
accesskit_macos = { path = "vendor/accesskit_macos" }
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
# builds, since every mutant rebuilds the crates above the one it changes.
[profile.mutants]
+1 -1
View File
@@ -48,7 +48,7 @@ impl Download {
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
let closed = ui_kit::Window::new(egui::Id::unique("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
+2 -2
View File
@@ -726,7 +726,7 @@ mod web {
return;
}
let mut act: Option<(&'static str, String)> = None;
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_files"), "folder", "Files in this browser")
// Room for long names; the kit still caps the window at the browser's width.
.cards(4.0)
.show_body(ctx, |ui| {
@@ -890,7 +890,7 @@ mod web {
fn recovery_prompt(&mut self, ctx: &egui::Context) {
let Some((name, path)) = self.files.recovery.borrow().clone() else { return };
let mut answer = None;
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_recovery"), "restore", "Recover unsaved work?")
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_recovery"), "restore", "Recover unsaved work?")
.required()
.show_body(ctx, |ui| {
ui.label(format!("“{name}” had unsaved changes when this page last closed."));
+1 -1
View File
@@ -1792,7 +1792,7 @@ mod viewport_tests {
viewport.fullscreen = Some(fullscreen);
viewport.native_pixels_per_point = Some(retina);
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
ctx.run_ui(raw, |ui| super::content_ui(app, ui)).textures_delta.clear();
};
// Enter, leave, and enter again: no stale inset when returning to a normal window.
for fullscreen in [false, true, false, true] {
+1 -1
View File
@@ -434,7 +434,7 @@ mod tests {
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
app.home.open = false;
let ctx = Context::default();
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {}).textures_delta.clear();
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
let queue = |bridge: &NavBridge| {
let mut shared = bridge.shared.lock().unwrap();
+1
View File
@@ -1914,6 +1914,7 @@ mod tests {
fn wheel() -> egui::Event {
egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Line,
delta: egui::vec2(0.0, -1.0),
modifiers: Default::default(),
+1 -2
View File
@@ -55,8 +55,7 @@ pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, fon
painter.ctx().add_plugin(LabelAtlas);
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
let atlas = painter.ctx().fonts(|f| f.font_image_size());
let primitives =
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
let primitives = painter.ctx().tessellate(vec![egui::epaint::ClippedShape::new(Rect::EVERYTHING, text)], painter.ctx().pixels_per_point());
for primitive in primitives {
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
mesh.texture_id = LABEL_PIXELS;
@@ -0,0 +1,13 @@
{
"en": { "ui.close_tab": "Close tab" },
"ar": { "ui.close_tab": "إغلاق علامة التبويب" },
"de": { "ui.close_tab": "Tab schließen" },
"es": { "ui.close_tab": "Cerrar pestaña" },
"fr": { "ui.close_tab": "Fermer l’onglet" },
"he": { "ui.close_tab": "סגירת הלשונית" },
"ja": { "ui.close_tab": "タブを閉じる" },
"ko": { "ui.close_tab": "탭 닫기" },
"vi": { "ui.close_tab": "Đóng thẻ" },
"zh": { "ui.close_tab": "关闭标签页" },
"zh-TW": { "ui.close_tab": "關閉分頁" }
}
@@ -23,7 +23,7 @@ fn blocks(ctx: &egui::Context, slot: usize, src: &str) -> Arc<Vec<Block>> {
let mut h = std::collections::hash_map::DefaultHasher::new();
src.hash(&mut h);
let key = h.finish();
let id = egui::Id::new(("agent_chat_markdown", slot));
let id = egui::Id::unique(("agent_chat_markdown", slot));
if let Some((k, b)) = ctx.data(|d| d.get_temp::<(u64, Arc<Vec<Block>>)>(id))
&& k == key
{
@@ -67,7 +67,7 @@ fn host(url: &str) -> &str {
pub(super) fn selectable(ui: &mut egui::Ui, label: egui::Label) -> egui::Response {
use egui::text_selection::LabelSelectionState;
let (pos, galley, response) = label.selectable(true).layout_in_ui(ui);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), galley.text()));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), galley.text()));
// As `egui::Label` draws a label that is not a button.
let colour = ui.visuals().text_color();
if ui.is_rect_visible(response.rect) {
+4 -1
View File
@@ -377,6 +377,8 @@ pub struct AgentChat {
pub panel_rect: Option<egui::Rect>,
/// A pointer press in the transcript owns keys just like a focused text editor.
keyboard_owned: bool,
/// The panel forgets its own width the next time it is drawn, so the preference applies.
pub(crate) forget_width: bool,
/// Unreserved sheet space, shared by the actual dock and its drag preview.
pub(crate) dock_space: Option<egui::Rect>,
/// The last message sent, kept to send again after a sign-in (its files too).
@@ -467,6 +469,7 @@ impl AgentChat {
pub(crate) fn swap_document(&mut self, other: &mut Self) {
std::mem::swap(self, other);
std::mem::swap(&mut self.open, &mut other.open);
std::mem::swap(&mut self.forget_width, &mut other.forget_width);
std::mem::swap(&mut self.following_document, &mut other.following_document);
std::mem::swap(&mut self.settings, &mut other.settings);
std::mem::swap(&mut self.control_starter, &mut other.control_starter);
@@ -2514,7 +2517,7 @@ pub fn control(app: &mut SolveApp, ctx: &egui::Context, p: &Value) -> Result<Val
}
if let Some(w) = p.get("width").and_then(Value::as_f64).filter(|w| w.is_finite()) {
app.preferences.agent_chat_width = (w as f32).clamp(panel::MIN_WIDTH, panel::MAX_WIDTH);
panel::forget_width(ctx);
app.agent_chat.forget_width = true;
}
if let Some(path) = p.get("attach").and_then(Value::as_str) {
attach_path(app, path)?;
+34 -32
View File
@@ -44,8 +44,8 @@ pub fn toggle_shortcut(app: &mut SolveApp, ctx: &egui::Context) {
}
}
fn panel_id(left: bool) -> Id {
Id::new(if left { "sc_agent_chat_left" } else { "sc_agent_chat_right" })
fn panel_salt(left: bool) -> &'static str {
if left { "sc_agent_chat_left" } else { "sc_agent_chat_right" }
}
/// The largest thumbnail of a message's picture, and its largest preview while hovered (points).
@@ -54,15 +54,6 @@ const THUMB_MAX: (f32, f32) = (150.0, 56.0);
#[cfg(not(target_arch = "wasm32"))]
const PREVIEW_MAX: (f32, f32) = (560.0, 380.0);
/// Forget the panel's own idea of its width, so the preference applies on the next frame.
pub fn forget_width(ctx: &egui::Context) {
ctx.data_mut(|d| {
for left in [true, false] {
d.remove::<egui::containers::panel::PanelState>(panel_id(left));
}
});
}
/// Show or hide the panel (a folded panel unfolds first).
pub fn toggle(app: &mut SolveApp) {
let c = &mut app.agent_chat;
@@ -123,7 +114,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
app.agent_chat.window_placed = false;
}
// New chat, when the chat has focus (its input, or the pointer over the panel).
let in_chat = ctx.memory(|m| m.has_focus(Id::new("sc_agent_chat_input")))
let in_chat = ctx.memory(|m| m.has_focus(Id::unique("sc_agent_chat_input")))
|| ctx.pointer_hover_pos().is_some_and(|p| app.agent_chat.panel_rect.is_some_and(|r| r.contains(p)));
if app.agent_chat.open && in_chat && ctx.input_mut(|i| i.consume_shortcut(&NEW_CHAT_SHORTCUT)) {
request_new_chat(app);
@@ -153,18 +144,27 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
let left = app.preferences.agent_chat_side == "left";
let host = Container::new(ui.ctx(), Surface::Panel);
let side = |id: Id| host.edge(id, if left { Edge::Left } else { Edge::Right });
let side = |salt: &'static str| host.edge(salt, if left { Edge::Left } else { Edge::Right });
if app.preferences.agent_chat_minimized {
let r = host
.show_edge(ui, side(Id::new("sc_agent_chat_strip")).exact_size(STRIP).resizable(false), host.edge_frame(t.panel), |ui| strip(app, ui));
let r = host.show_edge(ui, side("sc_agent_chat_strip").exact_size(STRIP).resizable(false), host.edge_frame(t.panel), |ui| strip(app, ui));
app.agent_chat.drop_rect = Some(r.response.rect);
show_drop_hint(app, &ctx);
return;
}
let width = app.preferences.agent_chat_width.clamp(MIN_WIDTH, MAX_WIDTH);
if std::mem::take(&mut app.agent_chat.forget_width) {
// A panel's state is keyed by the Ui it is shown in and its salt; either side's would
// override the preference once the panel moves there.
let scope = ui.scope_id();
ctx.data_mut(|d| {
for side in [true, false] {
d.remove::<egui::containers::panel::PanelState>(scope.with(panel_salt(side)));
}
});
}
let r = host.show_edge(
ui,
side(panel_id(left)).default_size(width).size_range(MIN_WIDTH..=MAX_WIDTH).resizable(true),
side(panel_salt(left)).default_size(width).size_range(MIN_WIDTH..=MAX_WIDTH).resizable(true),
host.edge_frame(t.dialog_body),
|ui| ui_kit::scope(ui, |ui| body(app, ui, left)),
);
@@ -254,7 +254,7 @@ fn floating(app: &mut SolveApp, ui: &mut egui::Ui) {
let anchor = if left { bounds.left_top() } else { bounds.right_top() };
let frame = ui_kit::Panel::feature_frame(&ctx, app.ui.layout, side);
let inner = (vec2(width, height) - frame.total_margin().sum()).max(egui::Vec2::ZERO);
let panel = ui_kit::Panel::new(tr("ui.assistant"), Id::new("sc_agent_chat_floating"), anchor, available, side, frame).width(width);
let panel = ui_kit::Panel::new(tr("ui.assistant"), Id::unique("sc_agent_chat_floating"), anchor, available, side, frame).width(width);
if let Some(card) = panel.fixed_window(&ctx, inner).show(&ctx, |ui| {
ui.set_min_size(inner);
ui.set_max_size(inner);
@@ -267,7 +267,7 @@ fn floating(app: &mut SolveApp, ui: &mut egui::Ui) {
} else {
Rect::from_x_y_ranges(rect.left()..=rect.left() + t.space_4, rect.y_range())
};
let response = ui.interact(rail, Id::new("sc_agent_chat_resize"), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
let response = ui.interact(rail, Id::unique("sc_agent_chat_resize"), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
crate::scenario::publish_handle("agent_chat:resize", rail.center());
if response.dragged() {
let delta = ui.input(|i| i.pointer.delta().x) * if left { 1.0 } else { -1.0 };
@@ -459,7 +459,7 @@ fn gallery_window(app: &mut SolveApp, ctx: &egui::Context) {
let mut step: isize = if left { -1 } else { isize::from(right) };
let t = Tokens::get();
let max = ctx.content_rect().size() * vec2(0.8, 0.7);
let modal = ui_kit::Container::new(ctx, ui_kit::Surface::Window).modal(Id::new("sc_agent_chat_gallery")).show(ctx, |ui| {
let modal = ui_kit::Container::new(ctx, ui_kit::Surface::Window).modal(Id::unique("sc_agent_chat_gallery")).show(ctx, |ui| {
ui.set_max_width(max.x);
ui.horizontal(|ui| {
let prev = ui_kit::button(ui, tr("ui.agent_chat_previous_picture"), ui_kit::Button::Enabled(count > 1));
@@ -517,7 +517,7 @@ fn show_drop_hint(app: &SolveApp, ctx: &egui::Context) {
#[cfg(not(target_arch = "wasm32"))]
fn drop_hint(ctx: &egui::Context, area: Rect) {
let t = Tokens::get();
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::new("sc_agent_chat_drop_hint")));
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::unique("sc_agent_chat_drop_hint")));
let zone = area.shrink(6.0);
painter.rect(zone, t.radius_xl, t.accent.gamma_multiply(0.16), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
let font = egui::FontId::proportional(Tokens::get().font_heading);
@@ -692,7 +692,7 @@ fn strip(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
let r = ui.max_rect();
let button = Rect::from_center_size(pos2(r.center().x, r.top() + 22.0), vec2(26.0, 26.0));
let resp = ui.interact(button, Id::new("sc_agent_chat_unfold"), Sense::click());
let resp = ui.interact(button, Id::unique("sc_agent_chat_unfold"), Sense::click());
crate::scenario::publish_handle("agent_chat:unfold", button.center());
if resp.hovered() {
ui.painter().rect_filled(button, t.radius_sm, t.hover);
@@ -750,7 +750,7 @@ fn header(app: &mut SolveApp, ui: &mut egui::Ui, left: bool) {
}
// Drag the header across the window to dock on the other side.
if !popped {
let drag = ui.interact(title, Id::new("sc_agent_chat_grip"), Sense::drag());
let drag = ui.interact(title, Id::unique("sc_agent_chat_grip"), Sense::drag());
crate::scenario::publish_handle("agent_chat:grip", title.center());
dock_drag(app, ui, &drag, left);
}
@@ -805,7 +805,7 @@ fn dock_drag(app: &mut SolveApp, ui: &egui::Ui, drag: &egui::Response, left: boo
};
crate::scenario::publish_rect("agent_chat:dock_hint", zone);
let t = Tokens::get();
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::new("sc_agent_chat_dock_hint")));
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::unique("sc_agent_chat_dock_hint")));
painter.rect(zone, t.radius_xl, t.accent.gamma_multiply(0.18), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
painter.text(
zone.center(),
@@ -877,7 +877,7 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
let busy = state == State::Busy;
// Enter sends (queues while the agent works); Ctrl/Cmd+Enter sends now; Esc stops. They are
// read before the field sees them so no newline is typed.
let focused = ui.memory(|m| m.has_focus(Id::new("sc_agent_chat_input")));
let focused = ui.memory(|m| m.has_focus(Id::unique("sc_agent_chat_input")));
let (steer, esc) = if focused {
ui.input_mut(|i| (i.consume_key(egui::Modifiers::COMMAND, egui::Key::Enter), busy && i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)))
} else {
@@ -886,7 +886,7 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
let (mut send_clicked, mut stop_clicked, mut enter, mut pick_clicked) = (false, false, false, false);
let mut remove_picked_at = None;
let frame = Container::new(ui.ctx(), Surface::Viewport).edge_frame(egui::Color32::TRANSPARENT);
let composer = Container::new(ui.ctx(), Surface::Viewport).edge(Id::new("sc_agent_chat_composer"), Edge::Bottom);
let composer = Container::new(ui.ctx(), Surface::Viewport).edge("sc_agent_chat_composer", Edge::Bottom);
composer.resizable(false).show_separator_line(false).frame(frame).show(ui, |ui| {
let footer = ui_kit::assistant_composer_footer(ui, |ui| {
let boxed = ui_kit::assistant_composer(ui, focused, |ui| {
@@ -912,7 +912,7 @@ fn desktop_body(app: &mut SolveApp, ui: &mut egui::Ui) {
.show(ui, |ui| {
ui_kit::assistant_input(
ui,
Id::new("sc_agent_chat_input"),
Id::unique("sc_agent_chat_input"),
&mut app.agent_chat.input,
tr(if busy { "ui.agent_chat_placeholder_busy" } else { "ui.agent_chat_placeholder" }),
)
@@ -1395,7 +1395,7 @@ fn config_select(app: &mut SolveApp, ui: &mut egui::Ui, option: &acp::ConfigOpti
}
response
};
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, enabled, format!("{name}: {selected}")));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::ComboBox, enabled, format!("{name}: {selected}")));
response.backing_on_hover_text(tip);
match picked {
Some(ui_kit::Picked::Listed(value)) => super::set_config_option(app, &option.id, &value),
@@ -1439,11 +1439,13 @@ mod config_ui_tests {
fn draw_switcher(app: &mut SolveApp, ctx: &egui::Context, width: f32) {
crate::scenario::clear_handles();
let screen = Rect::from_min_size(pos2(0.0, 0.0), vec2(width, 700.0));
let _ = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| {
ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| {
ui.allocate_ui(vec2(width - 16.0, 600.0), |ui| {
ui_kit::scope(ui, |ui| switcher(app, ui));
});
});
})
.textures_delta
.clear();
}
#[test]
@@ -1744,7 +1746,7 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
});
if copyable {
let drawn = egui::Rect::from_x_y_ranges(before.x_range(), before.min.y..=ui.cursor().min.y);
let resp = ui.interact(drawn, Id::new(("agent_chat_msg", n)), Sense::hover());
let resp = ui.interact(drawn, Id::unique(("agent_chat_msg", n)), Sense::hover());
copy_message_menu(ui, &resp, n, text);
}
}
@@ -1757,7 +1759,7 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
if !text.is_empty() {
copy_message_button(ui, n, text);
let drawn = egui::Rect::from_x_y_ranges(before.x_range(), before.min.y..=ui.cursor().min.y);
let resp = ui.interact(drawn, Id::new(("agent_chat_msg", n)), Sense::hover());
let resp = ui.interact(drawn, Id::unique(("agent_chat_msg", n)), Sense::hover());
copy_message_menu(ui, &resp, n, text);
}
}
@@ -1927,7 +1929,7 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
});
match (&editing, edit_buf.as_mut()) {
(Some((i, _)), Some(buf)) if *i == n => {
let te = ui_kit::message_field(ui, Id::new(("sc_agent_pending_input", n)), buf, "");
let te = ui_kit::message_field(ui, Id::unique(("sc_agent_pending_input", n)), buf, "");
crate::scenario::publish_rect("agent_chat:pending_input", te.rect);
ui.horizontal(|ui| {
if ui_kit::button(ui, tr("ui.agent_chat_save"), ui_kit::Button::Normal).clicked() {
+7 -7
View File
@@ -446,7 +446,7 @@ fn timeout_field(ui: &mut egui::Ui, secs: &mut u64, handle: &str) -> bool {
/// The free-text reasoning effort field with its hover hint; true when the text changed.
fn effort_field(ui: &mut egui::Ui, effort: &mut String, handle: &str) -> bool {
let response = ui_kit::cell_editor(ui, ui.id().with("ai_reasoning_effort"), effort, tr("ui.ai_reasoning_effort_field_hint"), false)
let response = ui_kit::cell_editor(ui, ui.scope_id().with("ai_reasoning_effort"), effort, tr("ui.ai_reasoning_effort_field_hint"), false)
.backing_on_hover_text(tr("ui.ai_reasoning_effort_hint"));
crate::scenario::publish_target(handle, response.rect);
response.changed()
@@ -529,7 +529,7 @@ fn connecting_section(app: &mut SolveApp, ui: &mut egui::Ui) {
/// Custom instructions (#8220): text every new chat's system note carries.
fn instructions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
ui_kit::settings_section(ui, "instructions", tr("ai.prefs.instructions"), |ui| {
let id = egui::Id::new("prefs_agent_instructions");
let id = egui::Id::unique("prefs_agent_instructions");
let text = &mut app.preferences.agent_instructions;
let response = ui_kit::text_area(ui, tr("ai.prefs.instructions"), id, text, 6, acp::INSTRUCTIONS_LIMIT);
if response.has_focus() {
@@ -1038,7 +1038,7 @@ pub fn dialog(app: &mut SolveApp, ctx: &egui::Context) {
return;
}
if let Some(url) = app.agent_chat.settings.add.as_ref().and_then(|s| s.confirm_insecure_http.clone()) {
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_ai_http_consent"), "status_warning", tr("ai.http.allow"))
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_ai_http_consent"), "status_warning", tr("ai.http.allow"))
.tone(ui_kit::Tone::Warning)
.actions(tr("ai.http.confirm"), tr("ui.cancel"), None)
.selected(ui_kit::Action::Cancel)
@@ -1074,7 +1074,7 @@ pub fn dialog(app: &mut SolveApp, ctx: &egui::Context) {
// Editing keeps the agent's kind: the kind switch is for adding only.
let editing = app.agent_chat.settings.add.as_ref().is_some_and(|s| s.editing.is_some());
let (title, ok) = if editing { ("ui.ai_edit_agent_title", "ui.ai_save") } else { ("ui.ai_add_agent_title", "ui.ai_add") };
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_ai_add"), "settings", tr(title))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_ai_add"), "settings", tr(title))
.cards(2.0)
.fields()
.actions(tr(ok), "Cancel", None)
@@ -1176,7 +1176,7 @@ pub fn dialog(app: &mut SolveApp, ctx: &egui::Context) {
ui_kit::text_field(ui, &mut s.name, "");
});
ui_kit::row(ui, tr("ui.ai_base_url"), |ui| {
let r = ui_kit::cell_editor(ui, egui::Id::new("sc_ai_add_url"), &mut s.base_url, tr("ui.ai_url_hint"), false);
let r = ui_kit::cell_editor(ui, egui::Id::unique("sc_ai_add_url"), &mut s.base_url, tr("ui.ai_url_hint"), false);
s.list_again |= r.lost_focus();
if r.changed() {
s.insecure_http_url = None;
@@ -1220,7 +1220,7 @@ pub fn dialog(app: &mut SolveApp, ctx: &egui::Context) {
ui_kit::row(ui, tr("ui.ai_key"), |ui| {
ui.add_enabled_ui(has_keychain, |ui| {
let hint = tr(if s.saved_key { "ui.ai_key_keep_hint" } else { "ui.ai_key_hint" });
let r = ui_kit::password_field(ui, egui::Id::new("sc_ai_add_key"), &mut s.key, hint);
let r = ui_kit::password_field(ui, egui::Id::unique("sc_ai_add_key"), &mut s.key, hint);
crate::scenario::publish_target("ai_add:key", r.rect);
s.list_again |= r.lost_focus();
});
@@ -2431,7 +2431,7 @@ mod card_tests {
h.frames(3);
h.step(&json!({"text":" vendor/own "})).unwrap();
h.frames(3);
assert!(h.ctx.memory(|m| m.has_focus(egui::Id::new("sc_ai_model:api-1").with("search"))), "the box keeps the focus over Preferences");
assert!(h.ctx.memory(|m| m.has_focus(egui::Id::unique("sc_ai_model:api-1").with("search"))), "the box keeps the focus over Preferences");
h.step(&json!({"click":{"handle":"prefs:ai_model:api-1:custom"}})).unwrap();
h.frames(3);
assert_eq!(model(&h).as_deref(), Some("vendor/own"));
+1 -1
View File
@@ -1687,7 +1687,7 @@ fn custom_instructions_reach_the_system_note_of_each_new_session() {
h.frames(4);
assert!(crate::scenario::rect_of("prefs:agent_instructions").is_some(), "the field is on the AI page");
h.app.preferences.ai_features = true;
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("prefs_agent_instructions")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("prefs_agent_instructions")));
h.frames(2);
h.step(&json!({"text": "M3"})).unwrap();
h.frames(2);
+2 -2
View File
@@ -485,7 +485,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
return;
}
let Some(p) = app.agent.pos else { return };
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Debug, egui::Id::new("sc_agent_cursor")));
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Debug, egui::Id::unique("sc_agent_cursor")));
for (at, t0) in &app.agent.ripples {
let k = ((app.now - t0) / 0.6).clamp(0.0, 1.0) as f32;
painter.circle_stroke(*at, 6.0 + 22.0 * k, Stroke::new(3.0 * (1.0 - k) + 0.5, agent().gamma_multiply(1.0 - k)));
@@ -495,7 +495,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
use crate::ui_kit::{Container, Surface};
let t = crate::theme::Tokens::get();
Container::new(ctx, Surface::Popup)
.area(egui::Id::new("sc_agent_command"))
.area(egui::Id::unique("sc_agent_command"))
.order(egui::Order::Debug)
.fixed_pos(p + vec2(15.0, 42.0))
.constrain(true)
+1 -1
View File
@@ -351,7 +351,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, shown: cra
let view = Tokens::view();
let to_screen = |p: [f64; 2]| image.min + vec2(p[0] as f32 * step, p[1] as f32 * step);
let painter = ui.painter_at(outer);
let shared: Shared = ui.ctx().data_mut(|d| d.get_temp_mut_or_default::<Shared>(egui::Id::new("body_gizmo_cache")).clone());
let shared: Shared = ui.ctx().data_mut(|d| d.get_temp_mut_or_default::<Shared>(egui::Id::unique("body_gizmo_cache")).clone());
let (hovered, handles) = shared
.lock()
.ok()
+1 -1
View File
@@ -131,7 +131,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let rows: Vec<Value> = bom["rows"].as_array().cloned().unwrap_or_default();
let (mut export, mut apply_row) = (None, None);
let closed =
crate::ui_kit::Window::new(egui::Id::new("sc_bom"), "bom", crate::i18n::tr_label("Bill of Materials")).cards(3.0).show_body(ctx, |ui| {
crate::ui_kit::Window::new(egui::Id::unique("sc_bom"), "bom", crate::i18n::tr_label("Bill of Materials")).cards(3.0).show_body(ctx, |ui| {
ui.horizontal_wrapped(|ui| {
let was = (d.top, d.by_definition, d.group_children);
let mut levels = usize::from(d.top);
+41 -35
View File
@@ -758,16 +758,16 @@ fn row_rect(ui: &egui::Ui) -> Rect {
pub(crate) use crate::ui_kit::{Mark, Row};
fn compact_tree(ui: &egui::Ui) -> bool {
ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::new("sc_compact_tree"))).unwrap_or(false)
ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::unique("sc_compact_tree"))).unwrap_or(false)
}
/// Where tree rows put their eye this frame (Preferences › Interface, #8718).
pub(crate) fn publish_row_prefs(app: &SolveApp, ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_tree_eye_left"), app.preferences.browser_eye_left));
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_tree_eye_left"), app.preferences.browser_eye_left));
}
fn eye_left(ui: &egui::Ui) -> bool {
ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::new("sc_tree_eye_left"))).unwrap_or(false)
ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::unique("sc_tree_eye_left"))).unwrap_or(false)
}
pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row<'_>) -> RowResp {
@@ -1134,7 +1134,7 @@ pub(crate) fn browser_width(remembered: f32) -> f32 {
pub(crate) fn resize_column(app: &mut SolveApp, ui: &egui::Ui, rect: Rect, width: f32, handle: &'static str) {
let rail = Rect::from_x_y_ranges(rect.right() - BROWSER_RAIL..=rect.right(), rect.y_range());
let response =
ui.interact(rail, egui::Id::new(("sc_shared_column_rail", handle)), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
ui.interact(rail, egui::Id::unique(("sc_shared_column_rail", handle)), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
if response.dragged() {
let delta = ui.input(|input| input.pointer.delta().x);
if delta.is_finite() {
@@ -1173,7 +1173,7 @@ pub(crate) fn reveals(app: &SolveApp) -> bool {
/// text edit started while it showed keeps it shown. A drag started elsewhere (orbiting the view)
/// passes over it without showing it.
fn reveal_opacity(app: &SolveApp, ctx: &egui::Context, previous: Option<Rect>, edge: Rect) -> f32 {
let card = egui::Id::new("sc_browser_card");
let card = egui::Id::unique("sc_browser_card");
let memory = card.with("reveal");
let was = ctx.data(|d| d.get_temp::<bool>(memory)).unwrap_or(false);
let over = ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| {
@@ -1245,7 +1245,7 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
let frame = container.frame(ui.ctx(), fill, crate::frame::Side::Left).inner_margin(CARD_INSET as i8);
let inner_width = (bounds.width() - frame.total_margin().sum().x).max(0.0);
let inner_height = (bounds.height() - frame.total_margin().sum().y).max(0.0);
let card = Container::new(ui.ctx(), Surface::Panel).area(egui::Id::new("sc_browser_card")).fixed_pos(bounds.min).show(ui.ctx(), |ui| {
let card = Container::new(ui.ctx(), Surface::Panel).area(egui::Id::unique("sc_browser_card")).fixed_pos(bounds.min).show(ui.ctx(), |ui| {
let limits = Rect::from_min_max(available.min, pos2(available.right(), bounds.bottom()));
let response = ui
.scope(|ui| {
@@ -1328,9 +1328,9 @@ fn render_card(app: &mut SolveApp, ui: &mut egui::Ui) {
let _ = app.run("view.render_browser", json!({"open":false}));
}
}
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("sc_compact_tree"), true));
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique("sc_compact_tree"), true));
browser_contents(app, ui, true);
ui.ctx().data_mut(|d| d.remove::<bool>(egui::Id::new("sc_compact_tree")));
ui.ctx().data_mut(|d| d.remove::<bool>(egui::Id::unique("sc_compact_tree")));
},
);
if (lens == 1) != app.render_workspace.view.by_look {
@@ -1367,8 +1367,9 @@ fn browser_collapsed_rail(app: &mut SolveApp, ui: &mut egui::Ui) {
let height = title_top + t.space_24 * 4.0 + t.space_8;
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
// Include the card margin in the hit target; keep the tint inside that margin.
let response =
ui.interact(rect.expand(CARD_INSET + 1.0), ui.id().with("browser_header"), Sense::click()).on_hover_cursor(egui::CursorIcon::PointingHand);
let response = ui
.interact(rect.expand(CARD_INSET + 1.0), ui.scope_id().with("browser_header"), Sense::click())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if response.hovered() || response.has_focus() {
crate::ui_kit::panel_header_hover(ui.painter(), rect);
}
@@ -1376,7 +1377,7 @@ fn browser_collapsed_rail(app: &mut SolveApp, ui: &mut egui::Ui) {
crate::ui_kit::disclosure(ui.painter(), disclosure.center(), false, t.text_dim);
crate::scenario::publish_handle("left-browser-toggle", disclosure.center());
crate::scenario::publish_target("browser:header", disclosure);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::tr("ui.browser_title")));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), crate::i18n::tr("ui.browser_title")));
if response.backing_on_hover_text(crate::i18n::tr("ui.browser_expand")).clicked() {
app.preferences.timeline_left_browser_collapsed = false;
}
@@ -1436,7 +1437,7 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) {
let collapsible = collapsible && !reveals(app);
let t = Tokens::get();
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER_H), Sense::hover());
let response = ui.interact(rect, ui.id().with("browser_header"), if collapsible { Sense::click() } else { Sense::hover() });
let response = ui.interact(rect, ui.scope_id().with("browser_header"), if collapsible { Sense::click() } else { Sense::hover() });
crate::scenario::publish_target("browser:header", rect);
let vertical = app.ui.show_timeline && app.preferences.timeline_left;
let stacked = crate::timeline_left::stacked(app);
@@ -1449,7 +1450,7 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) {
crate::ui_kit::panel_header_hover(ui.painter(), rect);
}
let title = crate::i18n::tr("ui.browser_title").to_string();
let kind = if collapsible { egui::WidgetType::Button } else { egui::WidgetType::Label };
let kind = if collapsible { egui::Role::Button } else { egui::Role::Label };
response.widget_info(|| egui::WidgetInfo::labeled(kind, true, &title));
let galley = ui.painter().layout_no_wrap(title, crate::fonts::semibold(t.font_heading), t.text);
ui.painter().galley(pos2(rect.left() + t.space_6, rect.center().y - galley.size().y / 2.0), galley, t.text);
@@ -1750,7 +1751,7 @@ fn component_rows_inner(
mark: if id == 0 && acts.lit.any() { Mark::Branch } else { Mark::None },
..Default::default()
};
let r = draw_row(ui, ui.id().with(("comp", id)), &row);
let r = draw_row(ui, ui.scope_id().with(("comp", id)), &row);
if r.fold {
toggle(app, key, true);
}
@@ -1897,7 +1898,7 @@ fn occurrence_rows_inner(
};
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row {
depth,
fold: Some(open),
@@ -2044,7 +2045,8 @@ fn occurrence_rows_inner(
fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
let key = "docsettings";
let open = is_open(app, key, false);
let r = draw_row(ui, ui.id().with(key), &Row { depth, fold: Some(open), icon: "settings", label: "Document Settings", ..Default::default() });
let r =
draw_row(ui, ui.scope_id().with(key), &Row { depth, fold: Some(open), icon: "settings", label: "Document Settings", ..Default::default() });
if r.fold || r.clicked {
toggle(app, key, false);
}
@@ -2052,7 +2054,7 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
return;
}
let units = format!("Units: {}", app.session.doc.units);
let r = draw_row(ui, ui.id().with("units"), &Row { depth: depth + 1, icon: "settings", label: &units, ..Default::default() });
let r = draw_row(ui, ui.scope_id().with("units"), &Row { depth: depth + 1, icon: "settings", label: &units, ..Default::default() });
if let Some(p) = r.secondary {
crate::context_menu::open_for(app, p, Target::Units);
} else if r.clicked
@@ -2066,7 +2068,7 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
crate::prefs::unit_scale(&app.session.doc.units).1,
format_args!("{:.*}", usize::from(app.preferences.angle_decimals), 0.0)
);
if draw_row(ui, ui.id().with("precision"), &Row { depth: depth + 1, icon: "settings", label: &prec, ..Default::default() }).clicked {
if draw_row(ui, ui.scope_id().with("precision"), &Row { depth: depth + 1, icon: "settings", label: &prec, ..Default::default() }).clicked {
if app.prefs_window.open {
crate::prefs::close(app, true);
} else {
@@ -2074,15 +2076,19 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
}
}
let params = format!("Parameters ({})", app.session.doc.params.len());
if draw_row(ui, ui.id().with("params"), &Row { depth: depth + 1, icon: "params", label: &params, ..Default::default() }).clicked {
if draw_row(ui, ui.scope_id().with("params"), &Row { depth: depth + 1, icon: "params", label: &params, ..Default::default() }).clicked {
if app.params.is_some() {
app.params = None;
} else {
app.start("parameters.change");
}
}
if draw_row(ui, ui.id().with("design_properties"), &Row { depth: depth + 1, icon: "settings", label: "Design Properties", ..Default::default() })
.clicked
if draw_row(
ui,
ui.scope_id().with("design_properties"),
&Row { depth: depth + 1, icon: "settings", label: "Design Properties", ..Default::default() },
)
.clicked
{
if app.design_properties.is_some() {
app.design_properties = None;
@@ -2113,7 +2119,7 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
let items = origin_things();
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row {
depth,
fold: Some(open),
@@ -2149,7 +2155,7 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
let (selected, mark) = lit_row(lit(&thing));
let r = draw_row(
ui,
ui.id().with(("o", k)),
ui.scope_id().with(("o", k)),
// Faded when hidden itself or with the whole Origin. A renamed plane's label is already
// in the interface's language (`name`).
&Row {
@@ -2197,7 +2203,7 @@ pub const STANDARD_VIEWS: [(&str, &str); 4] = [("Top", "top"), ("Front", "front"
fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut Actions) {
let key = "namedviews";
let open = is_open(app, key, false);
let r = draw_row(ui, ui.id().with(key), &Row { depth, fold: Some(open), icon: "folder", label: "Named Views", ..Default::default() });
let r = draw_row(ui, ui.scope_id().with(key), &Row { depth, fold: Some(open), icon: "folder", label: "Named Views", ..Default::default() });
if r.fold || r.clicked {
toggle(app, key, false);
}
@@ -2208,7 +2214,7 @@ fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m
return;
}
for (label, view) in STANDARD_VIEWS {
let r = draw_row(ui, ui.id().with(("stdview", view)), &Row { depth: depth + 1, icon: "home", label, ..Default::default() });
let r = draw_row(ui, ui.scope_id().with(("stdview", view)), &Row { depth: depth + 1, icon: "home", label, ..Default::default() });
if r.double {
app.animate_view(view);
}
@@ -2220,7 +2226,7 @@ fn named_view_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m
for name in names {
let r = draw_row(
ui,
ui.id().with(("view", &name)),
ui.scope_id().with(("view", &name)),
&Row { depth: depth + 1, icon: "perspective", label: &name, name: true, ..Default::default() },
);
if r.double {
@@ -2285,7 +2291,7 @@ fn analysis_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut
let open = is_open(app, key, start_open);
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row { depth, fold: Some(open), eye: Some(folder_shown), icon: "folder", label: "Analysis", dim: !folder_shown, ..Default::default() },
);
if r.fold || r.clicked {
@@ -2300,7 +2306,7 @@ fn analysis_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut
}
let r = draw_row(
ui,
ui.id().with("section_analysis"),
ui.scope_id().with("section_analysis"),
&Row {
depth: depth + 1,
eye: Some(section_shown),
@@ -2378,7 +2384,7 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
let shown = !app.session.folder_hidden(CANVASES);
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row { depth, fold: Some(open), eye: Some(shown), icon: "folder", label: "Canvases", dim: !shown, ..Default::default() },
);
if r.fold || r.clicked {
@@ -2394,7 +2400,7 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
let selected = app.tree.picked_canvases.contains(&id);
let r = draw_row(
ui,
ui.id().with(("canvas", id)),
ui.scope_id().with(("canvas", id)),
&Row {
depth: depth + 1,
eye: Some(visible),
@@ -2864,7 +2870,7 @@ fn folder_rows_at(
} else {
draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row {
depth,
fold: Some(open),
@@ -2975,7 +2981,7 @@ fn folder_level(
let count = if render && !gopen { crate::i18n::format("render.tree_more", &[("count", &group.count.to_string())]) } else { String::new() };
let r = draw_row(
ui,
ui.id().with(gkey),
ui.scope_id().with(gkey),
&Row {
depth,
fold: Some(gopen),
@@ -3076,7 +3082,7 @@ fn inline_rename(app: &mut SolveApp, ui: &mut egui::Ui, row: Rect, label_left: f
let Some(mut rename) = app.menu.rename.take() else { return };
let edit_rect = Rect::from_min_max(pos2(label_left, row.top() + 1.0), pos2(row.right() - 3.0, row.bottom() - 1.0));
ui.painter().rect_filled(edit_rect, 3.0, Tokens::get().field);
let id = ui.id().with(("browser_rename", &e.row_key));
let id = ui.scope_id().with(("browser_rename", &e.row_key));
let response = crate::ui_kit::inline_name(ui, edit_rect, id, &mut rename.text);
crate::scenario::publish_handle(&format!("rename:{}", e.key), edit_rect.center());
let was_focused = rename.focused;
@@ -3148,7 +3154,7 @@ fn entry_row(
let state_tip = sketch_state.map(crate::sketch_state::SketchState::text);
let r = draw_row(
ui,
ui.id().with(("item", &e.row_key)),
ui.scope_id().with(("item", &e.row_key)),
&Row {
depth,
fold: pattern_key.map(|_| patterns_open),
@@ -3543,7 +3549,7 @@ pub(crate) fn occurrence_triad(app: &mut SolveApp, ui: &mut egui::Ui, painter: &
let before = (m.translate, m.angles);
let Some(t) = occurrence_triad_model(app) else { return };
let step = crate::canvas::snap_step(app.cam.half_height());
let Some(g) = gizmo::show(ui, painter, proj, egui::Id::new("sc_occ_triad"), &t, step) else { return };
let Some(g) = gizmo::show(ui, painter, proj, egui::Id::unique("sc_occ_triad"), &t, step) else { return };
if g.started || m.drag0.is_none() {
m.drag0 = Some((m.translate, m.angles));
}
+7 -4
View File
@@ -114,8 +114,11 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
.collect()
};
let (status, tip) = crate::timeline::worst_status(app, &ids);
let r =
draw_row(ui, ui.id().with(&key), &Row { depth, fold: Some(open), icon: "folder", label, status, tip: tip.as_deref(), ..Default::default() });
let r = draw_row(
ui,
ui.scope_id().with(&key),
&Row { depth, fold: Some(open), icon: "folder", label, status, tip: tip.as_deref(), ..Default::default() },
);
if r.fold || r.clicked {
toggle(app, &key, compact);
}
@@ -159,7 +162,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
let mut slots: Vec<(f32, usize)> = Vec::new();
// Where dragged rows can be dropped, as in the History at the left: these slots and one
// before the marker, each with the folder the rows join (#9565).
let drag_id = ui.id().with(("sc_browser_history_drag", &scope));
let drag_id = ui.scope_id().with(("sc_browser_history_drag", &scope));
let mut dragged = app.tree.content.current_mut().feature_drags.get(&drag_id).cloned();
let mut drops: Vec<DropSlot> = Vec::new();
let mut marker_resp = None;
@@ -289,7 +292,7 @@ fn feature_row(
let cut = app.cut.features.contains(&id);
let r = draw_row(
ui,
ui.id().with(("browser_feature", id)),
ui.scope_id().with(("browser_feature", id)),
&Row {
depth,
fold,
+3 -3
View File
@@ -214,7 +214,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m
let shown = !app.session.folder_hidden(POINT_CLOUDS_FOLDER);
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row { depth, fold: Some(open), eye: Some(shown), icon: "folder", label: "Point Clouds", dim: !shown, ..Default::default() },
);
if r.fold || r.clicked {
@@ -229,7 +229,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &m
for (id, name, visible) in clouds {
let r = draw_row(
ui,
ui.id().with(("point_cloud", id)),
ui.scope_id().with(("point_cloud", id)),
&Row { depth: depth + 1, eye: Some(visible), icon: "point", label: &name, name: true, dim: !visible || !shown, ..Default::default() },
);
if r.eye {
@@ -289,7 +289,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
};
let changes_of = c.colored_property().filter(|n| c.complex.integer(n)).map(str::to_string);
let at = panel_pos(app);
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_point_cloud_panel"), "point", &title)
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_point_cloud_panel"), "point", &title)
.placed(egui::Align2::LEFT_TOP, at)
.fields()
.actions("", crate::i18n::tr("ui.close"), None)
+1 -1
View File
@@ -109,7 +109,7 @@ const RULER: f32 = 14.0;
const BARS: f32 = 40.0;
fn id(key: &str) -> egui::Id {
egui::Id::new(("sc_range_track", key))
egui::Id::unique(("sc_range_track", key))
}
fn state(ctx: &egui::Context, key: &str) -> State {
+1 -1
View File
@@ -26,7 +26,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
let at = panel_pos(app);
let (calibrate, name) = (m.calibrate, c.id);
let (ok, cancel) = if calibrate { (crate::i18n::tr_label("Calibrate"), crate::i18n::tr("ui.cancel")) } else { ("", crate::i18n::tr("ui.close")) };
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_canvas_panel"), "canvas", &title)
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_canvas_panel"), "canvas", &title)
.placed(egui::Align2::LEFT_TOP, at)
.fields()
.actions(ok, cancel, None)
+4 -4
View File
@@ -323,7 +323,7 @@ fn selected_row_paints_tab_soft_fill_and_accent_text_in_both_layouts() {
let mut row_rect = Rect::NOTHING;
let out = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
let row = Row { selected: true, label: "Selected body", dim: true, color: Some(Tokens::get().error), ..Default::default() };
row_rect = draw_row(ui, egui::Id::new("selected-row"), &row).rect.unwrap();
row_rect = draw_row(ui, egui::Id::unique("selected-row"), &row).rect.unwrap();
});
assert_eq!(row_rect.height(), 26.0);
assert!(out.shapes.iter().any(|s| matches!(&s.shape, egui::Shape::Rect(r) if r.rect == row_rect && r.fill == accent.soft)));
@@ -1104,7 +1104,7 @@ fn semantic_frame_transitions(
semantic_content::finalize(&mut h.app, ui.ctx());
// App paths may converge after an early return; finalization must be idempotent.
semantic_content::finalize(&mut h.app, ui.ctx());
if ui.ctx().data_mut(|data| data.remove_temp::<bool>(egui::Id::new("semantic-test-discard"))).unwrap_or(false) {
if ui.ctx().data_mut(|data| data.remove_temp::<bool>(egui::Id::unique("semantic-test-discard"))).unwrap_or(false) {
ui.ctx().request_discard("repeat semantic layout after input");
}
})
@@ -1516,7 +1516,7 @@ fn semantic_row_commands_are_consumed_once_across_layout_repeats() {
let undo = h.app.session.undo.len();
semantic_frame(&mut h, &owners, vec![egui::Event::PointerMoved(at)], egui::Modifiers::NONE);
semantic_frame(&mut h, &owners, vec![semantic_button(at, egui::PointerButton::Primary, true, egui::Modifiers::NONE)], egui::Modifiers::NONE);
h.ctx.data_mut(|data| data.insert_temp(egui::Id::new("semantic-test-discard"), true));
h.ctx.data_mut(|data| data.insert_temp(egui::Id::unique("semantic-test-discard"), true));
semantic_frame(&mut h, &owners, vec![semantic_button(at, egui::PointerButton::Primary, false, egui::Modifiers::NONE)], egui::Modifiers::NONE);
assert!(body_hidden(&h, "One"), "a repeated layout cannot toggle the eye back on");
assert_eq!(h.app.session.undo.len(), undo + 1);
@@ -1802,7 +1802,7 @@ fn switching_semantic_documents_preserves_unrelated_payload_and_capture() {
}
let from = semantic_at(ContentOwner::BROWSER, "One");
semantic_drag_start(&mut h, &owners, from);
let external = egui::Id::new("external capture");
let external = egui::Id::unique("external capture");
egui::DragAndDrop::set_payload(&h.ctx, "external payload".to_string());
h.ctx.set_dragged_id(external);
crate::documents::switch(&mut h.app, tab);
+2 -2
View File
@@ -41,7 +41,7 @@ pub(crate) fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
let title = crate::i18n::format("browser.move_title", &[("name", &name)]);
let at = panel_pos(app);
let (units, decimals) = (app.session.doc.units.clone(), app.preferences.length_decimals);
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_occ_move"), "move", &title)
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_occ_move"), "move", &title)
.placed(egui::Align2::LEFT_TOP, at)
.fields()
.cards(2.0)
@@ -97,7 +97,7 @@ pub(crate) fn capture_prompt(app: &mut SolveApp, ctx: &egui::Context) {
return;
}
let n = app.session.pending_moves.len();
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_capture_prompt"), "capture", crate::i18n::tr_label("Capture Position"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_capture_prompt"), "capture", crate::i18n::tr_label("Capture Position"))
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, -80.0))
.actions("Capture Position", "Cancel", Some("Revert"))
.show(ctx, true, |ui| {
+32 -28
View File
@@ -375,7 +375,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
if matches!(d.kind, Kind::Move { .. }) {
let mut dragging = false;
if let Some(t) = crate::dialogs_move::triad(app, &d)
&& let Some(g) = crate::gizmo::show(ui, painter, proj, egui::Id::new("sc_move_triad"), &t, snap_step(app.cam.half_height()))
&& let Some(g) = crate::gizmo::show(ui, painter, proj, egui::Id::unique("sc_move_triad"), &t, snap_step(app.cam.half_height()))
{
crate::dialogs_move::drag(app, &mut d, g);
dragging = true;
@@ -396,7 +396,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj
// Tab and Shift+Tab in the box show the next or previous value (Y after X; #4776),
// keeping the keyboard in the box.
// egui moves the keyboard on with Tab before this runs: the box had it last frame.
let id = egui::Id::new("sc_canvas_value");
let id = egui::Id::unique("sc_canvas_value");
let boxed = ui.ctx().memory(|m| m.has_focus(id) || m.had_focus_last_frame(id));
let back = boxed && ui.input_mut(|i| i.consume_key(egui::Modifiers::SHIFT, egui::Key::Tab));
let tab = boxed && !back && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab));
@@ -619,7 +619,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
-(n * sign),
l2,
true,
egui::Id::new("sc_manipulator2"),
egui::Id::unique("sc_manipulator2"),
half_height,
0.0,
distance2,
@@ -630,7 +630,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
if let (Kind::Fillet { distance2, .. }, Some((_, t2))) = (&mut d.kind, faces)
&& let Some(l2) = crate::length_input::eval(&app.session.doc, distance2, ValueKind::Length).ok().filter(|v| v.is_finite())
{
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, t2, l2, false, egui::Id::new("sc_manipulator2"), half_height, 0.0, distance2);
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, t2, l2, false, egui::Id::unique("sc_manipulator2"), half_height, 0.0, distance2);
}
// A rectangular pattern's second direction gets its own arrow for Spacing 2 (#2462), as the
// first direction's arrow sets Spacing.
@@ -642,7 +642,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
if let Some(spacing2) = spacing2
&& let Some(l2) = crate::length_input::eval(&app.session.doc, spacing2, ValueKind::Length).ok().filter(|v| v.is_finite())
{
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, n2, l2, true, egui::Id::new("sc_manipulator2"), half_height, 0.0, spacing2);
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, n2, l2, true, egui::Id::unique("sc_manipulator2"), half_height, 0.0, spacing2);
}
}
// An offset plane shows where it will be: at the distance, or through the picked point.
@@ -700,7 +700,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
crate::length_input::eval(&app.session.doc, depth, ValueKind::Length).ok().filter(|v| v.is_finite()).unwrap_or(0.0)
};
let before = depth.clone();
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, -n, l, false, egui::Id::new("sc_manipulator_depth"), half_height, 0.0, depth);
drag_arrow(&app.session.doc, ui, painter, proj, base, bs, -n, l, false, egui::Id::unique("sc_manipulator_depth"), half_height, 0.0, depth);
// Dragging the depth turns "through all" into a distance.
if *depth != before {
opts.all = false;
@@ -735,7 +735,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
-n,
l2.abs(),
false,
egui::Id::new("sc_manipulator_offset2"),
egui::Id::unique("sc_manipulator_offset2"),
half_height,
0.0,
distance2,
@@ -743,7 +743,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
// Its own area and field, so it does not share Distance's focus or text.
value_box_in(
ui,
(egui::Id::new("sc_canvas_area2"), egui::Id::new("sc_canvas_value2")),
(egui::Id::unique("sc_canvas_area2"), egui::Id::unique("sc_canvas_value2")),
"Distance 2",
distance2,
(ts + vec2(14.0, 8.0), egui::Align2::LEFT_TOP),
@@ -785,7 +785,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
n,
half + baseline,
signed,
egui::Id::new("sc_manipulator"),
egui::Id::unique("sc_manipulator"),
half_height,
clear,
dragged,
@@ -811,7 +811,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
// Clear of the command window first, then off the placement handles: the handles have the
// last word, as a box over a grip takes the press meant for it (#6608).
let place = box_place(app, proj);
let size = box_size(ui, egui::Id::new("sc_canvas_area"), label, note.as_deref());
let size = box_size(ui, egui::Id::unique("sc_canvas_area"), label, note.as_deref());
let box_at = clear_of_dialog(rect_at(box_at, egui::Align2::LEFT_TOP, size), place).min;
let box_at = crate::place_gizmo::yield_box(ui, anchor, box_at);
value_box_noted(ui, label, value, box_at, focus || *value != before, note.as_deref(), BoxPlace { dialog: None, ..place });
@@ -841,7 +841,7 @@ pub(crate) fn box_place(app: &SolveApp, proj: &Proj) -> BoxPlace {
}
fn boxes_shown_id() -> egui::Id {
egui::Id::new("sc_canvas_value_boxes")
egui::Id::unique("sc_canvas_value_boxes")
}
/// Show commands' values in boxes in the view or not, from the preference (the app calls this
@@ -878,13 +878,13 @@ fn value_box(ui: &mut egui::Ui, label: &str, value: &mut String, at: Pos2, selec
/// [`value_box`]; with `keep_tab` Tab stays in the box for the caller (the Move box steps
/// through its values with it).
fn value_box_with(ui: &mut egui::Ui, label: &str, value: &mut String, at: Pos2, select_all: bool, keep_tab: bool, place: BoxPlace) {
let ids = (egui::Id::new("sc_canvas_area"), egui::Id::new("sc_canvas_value"));
let ids = (egui::Id::unique("sc_canvas_area"), egui::Id::unique("sc_canvas_value"));
value_box_in(ui, ids, label, value, (at, egui::Align2::LEFT_TOP), select_all, keep_tab, place);
}
/// [`value_box`] with a note after the field (a pull's step size).
fn value_box_noted(ui: &mut egui::Ui, label: &str, value: &mut String, at: Pos2, select_all: bool, note: Option<&str>, place: BoxPlace) {
let ids = (egui::Id::new("sc_canvas_area"), egui::Id::new("sc_canvas_value"));
let ids = (egui::Id::unique("sc_canvas_area"), egui::Id::unique("sc_canvas_value"));
value_box_full(ui, ids, label, value, (at, egui::Align2::LEFT_TOP), (select_all, false), note, place);
}
@@ -1133,7 +1133,7 @@ fn drag_arrow(
}
// Paint the value just read from this frame's pointer, including the value box anchor.
// Using the old distance here made both trail the drag by another frame.
crate::scenario::publish_handle(if id == egui::Id::new("sc_manipulator") { "arrow" } else { "arrow2" }, ts);
crate::scenario::publish_handle(if id == egui::Id::unique("sc_manipulator") { "arrow" } else { "arrow2" }, ts);
let hot = r.hovered() || r.dragged();
let col = if hot { t.accent } else { t.manipulator };
arrow(painter, start(ts), ts, col);
@@ -1264,7 +1264,8 @@ fn radius_spline_handles(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Pain
});
if let Some((p, u)) = near {
crate::scenario::publish_handle("fillet_curve", p);
if ui.interact(egui::Rect::from_center_size(p, vec2(12.0, 12.0)), egui::Id::new("sc_fillet_curve"), egui::Sense::click()).double_clicked() {
if ui.interact(egui::Rect::from_center_size(p, vec2(12.0, 12.0)), egui::Id::unique("sc_fillet_curve"), egui::Sense::click()).double_clicked()
{
edit = Some(Edit::Insert(u));
}
}
@@ -1284,8 +1285,11 @@ fn radius_spline_handles(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Pain
for (k, (u, ts)) in g.iter().zip(&tips).enumerate() {
let Some(ts) = *ts else { continue };
crate::scenario::publish_handle(&if i == 0 { format!("fillet_r{k}") } else { format!("fillet_e{i}_r{k}") }, ts);
let r =
ui.interact(egui::Rect::from_center_size(ts, vec2(16.0, 16.0)), egui::Id::new(("sc_fillet_r", i, k)), egui::Sense::click_and_drag());
let r = ui.interact(
egui::Rect::from_center_size(ts, vec2(16.0, 16.0)),
egui::Id::unique(("sc_fillet_r", i, k)),
egui::Sense::click_and_drag(),
);
let hot = r.hovered() || r.dragged();
let col = if hot { t.accent } else { t.manipulator };
if let Some(fs) = proj.to_screen(at(*u)) {
@@ -1386,7 +1390,7 @@ fn drag_label(ui: &mut egui::Ui, labels: &mut MeasureLabels, i: usize, rect: egu
if let Some(drawn) = labels.drawn.get_mut(i) {
*drawn = Some(rect);
}
let r = ui.interact(rect, egui::Id::new(("sc_measure_label", i)), egui::Sense::click_and_drag());
let r = ui.interact(rect, egui::Id::unique(("sc_measure_label", i)), egui::Sense::click_and_drag());
if r.dragged() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
} else if r.hovered() {
@@ -1629,7 +1633,7 @@ fn taper_handle(
let l = span.len();
let end = foot + span + e * (l * angle.tan().clamp(-10.0, 10.0));
let (Some(fs), Some(es)) = (proj.to_screen(foot), proj.to_screen(end)) else { return };
let r = ui.interact(egui::Rect::from_center_size(es, vec2(16.0, 16.0)), egui::Id::new("sc_taper"), egui::Sense::drag());
let r = ui.interact(egui::Rect::from_center_size(es, vec2(16.0, 16.0)), egui::Id::unique("sc_taper"), egui::Sense::drag());
crate::scenario::publish_handle("taper", es);
let hot = r.hovered() || r.dragged();
let col = if hot { t.accent } else { t.manipulator };
@@ -1811,7 +1815,7 @@ fn draw_revolve(
// This badge is read-only. Its placement follows the model and can overlap the
// command dialog, whose buttons must keep receiving clicks.
let area = Container::new(ui.ctx(), Surface::Canvas)
.area(egui::Id::new("sc_canvas_area"))
.area(egui::Id::unique("sc_canvas_area"))
.fixed_pos(box_at)
.order(egui::Order::Background)
.interactable(false);
@@ -1911,7 +1915,7 @@ fn rotation_handle(
) -> Option<Pos2> {
let end = proj.to_screen(centre + (e1 * boundary.cos() + e2 * boundary.sin()) * radius)?;
crate::scenario::publish_handle(name, end);
let response = ui.interact(egui::Rect::from_center_size(end, vec2(20.0, 20.0)), egui::Id::new(("sc_rotator", name)), egui::Sense::drag());
let response = ui.interact(egui::Rect::from_center_size(end, vec2(20.0, 20.0)), egui::Id::unique(("sc_rotator", name)), egui::Sense::drag());
let hot = response.hovered() || response.dragged();
let color = if hot { Tokens::get().accent } else { Tokens::get().manipulator };
painter.circle(end, if hot { 7.0 } else { 5.5 }, color, Stroke::new(1.5, Color32::WHITE));
@@ -2067,7 +2071,7 @@ mod tests {
h.app.session.selection = vec![Sel::Body { name: "Body1".into() }];
h.step(&serde_json::json!({"start": "solid.move"})).unwrap();
h.frames(6);
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).expect("the value box");
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).expect("the value box");
let window = h.app.viewport.dialog_rect.expect("the command window");
// Drag the command window over the value box: the box must move out of it.
let title = crate::scenario::handle_at("dialog:title").expect("the window's title");
@@ -2076,7 +2080,7 @@ mod tests {
h.frames(6);
let window = h.app.viewport.dialog_rect.unwrap();
assert!(window.contains(box_rect.center()), "the window {window:?} did not reach the value box {box_rect:?}");
let kept = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).expect("the value box");
let kept = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).expect("the value box");
assert!(!kept.intersects(window), "the value box {kept:?} covers the command window {window:?}");
}
@@ -2100,7 +2104,7 @@ mod tests {
{"click": {"world": [30, 15, 10]}}
]))
.unwrap();
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).expect("the value box");
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).expect("the value box");
let handles = crate::scenario::handles("move_");
assert!(!handles.is_empty(), "the triad drew no handles");
for (name, p) in &handles {
@@ -2134,17 +2138,17 @@ mod tests {
h.step(&serde_json::json!({"start": "solid.move"})).unwrap();
h.frames(6);
let order = |h: &crate::scenario::Harness| {
h.ctx.memory(|m| m.areas().visible_layer_ids().into_iter().find(|l| l.id == egui::Id::new("sc_canvas_area")).map(|l| l.order))
h.ctx.memory(|m| m.areas().visible_layer_ids().into_iter().find(|l| l.id == egui::Id::unique("sc_canvas_area")).map(|l| l.order))
};
assert_eq!(order(&h), Some(egui::Order::Background), "the value box belongs under system windows");
h.call("ui.prefs", serde_json::json!({"open": "General"}));
h.frames(3);
let window = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_prefs"))).expect("Preferences");
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).expect("the value box");
let window = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_prefs"))).expect("Preferences");
let box_rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).expect("the value box");
assert!(order(&h) < Some(egui::Order::Middle), "Preferences draws over the value box");
if window.intersects(box_rect) {
assert!(
h.ctx.memory(|m| m.layer_id_at(box_rect.center())).is_some_and(|l| l.id == egui::Id::new("sc_prefs")),
h.ctx.memory(|m| m.layer_id_at(box_rect.center())).is_some_and(|l| l.id == egui::Id::unique("sc_prefs")),
"Preferences covers the Distance box where they overlap"
);
}
+1 -1
View File
@@ -86,7 +86,7 @@ fn every_axis_is_drawn_in_one_set_of_colours() {
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(rect), ..Default::default() }, |ui| {
let painter = ui.painter().clone();
crate::gizmo::show(ui, &painter, &proj, egui::Id::new("one_set_gizmo"), &triad, 1.0);
crate::gizmo::show(ui, &painter, &proj, egui::Id::unique("one_set_gizmo"), &triad, 1.0);
});
out.textures_delta.clear();
let mut gizmo = stroke_colours(&out.shapes);
+1 -1
View File
@@ -190,7 +190,7 @@ pub(super) fn configurations_rows(
.unwrap_or_default()
});
let (kind, _) = column_parts(column);
let (response, choice) = value_control(ui, ui.id().with(column), &mut value, kind, &material_names);
let (response, choice) = value_control(ui, ui.scope_id().with(column), &mut value, kind, &material_names);
let picked = choice.is_some();
if let Some(choice) = choice {
value = choice;
+4 -4
View File
@@ -2568,7 +2568,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
inside |= pointer.is_some_and(|p| area.contains(p));
let hot = pointer.and_then(|p| sector(p - c));
let resp = Container::new(ctx, Surface::Popup)
.area(egui::Id::new("sc_marking_menu"))
.area(egui::Id::unique("sc_marking_menu"))
.fixed_pos(area.min)
.order(egui::Order::Foreground)
.show(ctx, |ui| {
@@ -2615,7 +2615,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let key_row = app.menu.key_row;
let mut seen: Vec<Seen> = Vec::new();
let resp = Container::new(ctx, Surface::Popup)
.area(egui::Id::new("sc_context_menu"))
.area(egui::Id::unique("sc_context_menu"))
.fixed_pos(list_at)
.constrain(true)
.order(egui::Order::Foreground)
@@ -2632,7 +2632,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let Some(row) = seen.get(depth - 1).and_then(|s| s.anchor) else { break };
// One area per flyout and contents: an egui area keeps its last size, so a flyout sharing
// one with a shorter one would be squeezed into it and scroll.
let id = egui::Id::new(("sc_context_submenu", depth)).with(rows.iter().map(|i| (&i.id, &i.label)).collect::<Vec<_>>());
let id = egui::Id::unique(("sc_context_submenu", depth)).with(rows.iter().map(|i| (&i.id, &i.label)).collect::<Vec<_>>());
let cursor = if depth == path.len() { app.menu.submenu_key } else { None };
let sub = crate::ui_kit::Menu::submenu(ctx, id, row, list_height(rows), leftward, |ui| {
egui::ScrollArea::vertical()
@@ -2823,7 +2823,7 @@ fn show_rename(app: &mut SolveApp, ctx: &egui::Context) {
let Some(mut r) = app.menu.rename.take() else { return };
let mut done: Option<bool> = None;
let resp =
Container::new(ctx, Surface::Popup).area(egui::Id::new("sc_rename")).fixed_pos(r.at).constrain(true).order(egui::Order::Foreground).show(
Container::new(ctx, Surface::Popup).area(egui::Id::unique("sc_rename")).fixed_pos(r.at).constrain(true).order(egui::Order::Foreground).show(
ctx,
|ui| {
done = crate::ui_kit::rename_menu(ui, &mut r.text, &mut r.focused);
+7 -1
View File
@@ -1282,7 +1282,13 @@ fn dispatch(app: &mut SolveApp, ctx: &egui::Context, method: &str, p: &Value) ->
if explicit {
app.synthetic.push(egui::Event::ModifiersChanged(modifiers));
}
app.synthetic.push(egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, delta), modifiers, phase: egui::TouchPhase::Move });
app.synthetic.push(egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit,
delta: egui::vec2(0.0, delta),
modifiers,
phase: egui::TouchPhase::Move,
});
if explicit {
app.synthetic.push(egui::Event::ModifiersChanged(egui::Modifiers::NONE));
}
+2 -2
View File
@@ -76,7 +76,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
let mut report: Option<String> = None;
let mut delete: Option<Option<String>> = None;
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_crash_reports"), "bug_report", crate::i18n::tr_label("Crash Reports"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_crash_reports"), "bug_report", crate::i18n::tr_label("Crash Reports"))
.cards(2.0)
.actions("", crate::i18n::tr("ui.close"), None)
.show(ctx, true, |ui| {
@@ -281,7 +281,7 @@ mod tests {
h.app.crashes.folder = Some(d.clone());
super::open(&mut h.app);
h.frames(6);
let window = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_crash_reports"))).unwrap();
let window = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_crash_reports"))).unwrap();
// The frame's margins come on top of the content's maximum width.
assert!(window.width() <= 760.0 + 40.0, "{} wide", window.width());
let _ = std::fs::remove_dir_all(&d);
+1 -1
View File
@@ -185,7 +185,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
};
let (deleted, fails) = (list("deleted"), list("would_fail"));
let shared = shared_occurrences(app, &p.params);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_delete_confirm"), "delete", crate::i18n::tr_label("Delete"))
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_delete_confirm"), "delete", crate::i18n::tr_label("Delete"))
.tone(ui_kit::Tone::Danger)
.actions("Delete", "Cancel", None)
.show(ctx, true, |ui| {
+5 -5
View File
@@ -87,12 +87,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let Some(mut d) = app.design_properties.take() else { return };
let metadata = app.session.doc.metadata.clone();
let title = crate::i18n::tr_label("Design Properties");
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_design_properties"), "settings", title).fields().show(ctx, true, |ui| {
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_design_properties"), "settings", title).fields().show(ctx, true, |ui| {
for (i, k) in keys(Owner::Design).enumerate() {
let (key, label) = (k.name, crate::i18n::tr(k.label));
let Some(field) = d.fields.get_mut(i) else { continue };
if key == DESCRIPTION {
ui_kit::multiline_text_field(ui, label, egui::Id::new(("sc_design_properties", key)), field);
ui_kit::multiline_text_field(ui, label, egui::Id::unique(("sc_design_properties", key)), field);
} else {
ui_kit::row(ui, label, |ui| ui_kit::text_field(ui, field, ""));
}
@@ -253,7 +253,7 @@ fn show_measuring(app: &mut SolveApp, ctx: &egui::Context) {
Some((done, total)) => crate::i18n::format("ui.properties.measuring_bodies", &[("done", &done.to_string()), ("total", &total.to_string())]),
None => app.tr("ui.properties.measuring").to_string(),
};
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
ui.horizontal(|ui| {
ui.spinner();
ui_kit::text(ui, &text, ui_kit::Text::Note("measuring"));
@@ -435,7 +435,7 @@ pub fn show_object(app: &mut SolveApp, ctx: &egui::Context) {
let copy = shown_tensor(app, total).map(|t| tensor_text(&t));
let copy_label = app.tr("ui.properties.copy_matrix").to_string();
let heading = window_title(&title, &v);
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
for (label, value) in &rows {
ui_kit::row(ui, crate::i18n::tr_label(label), |ui| ui_kit::text(ui, value, ui_kit::Text::Cell));
}
@@ -738,7 +738,7 @@ mod tests {
crate::context_menu::run_item(&mut h.app, item, egui::pos2(0.0, 0.0));
assert!(h.app.design_properties.is_some());
h.app.design_properties = None;
h.ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_file_menu"), true));
h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_file_menu"), true));
h.frames(2);
h.step(&json!({"widget": "Design Properties…"})).unwrap();
assert!(h.app.design_properties.is_some());
+9 -9
View File
@@ -2081,7 +2081,7 @@ pub(crate) fn fillet_least_setback(ctx: &egui::Context, s: &Session, items: &[Se
(&s.document_id, s.revision, radius.to_bits(), &bodies).hash(&mut key);
corners.iter().for_each(|c| [c.x, c.y, c.z].map(f64::to_bits).hash(&mut key));
let key = key.finish();
let id = egui::Id::new("fillet_least_setback");
let id = egui::Id::unique("fillet_least_setback");
if let Some((k, least)) = ctx.data(|d| d.get_temp::<(u64, Option<f64>)>(id))
&& k == key
{
@@ -2539,7 +2539,7 @@ fn tr(text: &str) -> &str {
}
fn publish_check(ui: &egui::Ui, response: &egui::Response, selected: bool) {
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), selected, tr(crate::ui_kit::row_label(ui))));
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::CheckBox, ui.is_enabled(), selected, tr(crate::ui_kit::row_label(ui))));
}
fn title(k: &Kind) -> &'static str {
@@ -3024,7 +3024,7 @@ pub(crate) fn input_rows_range(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, n
let chip = crate::ui_kit::selection_field(ui, prompt, crate::ui_kit::Selection { active, filled });
if matches!(d.kind, Kind::Shell { .. }) && i == 0 {
chip.widget_info(|| {
let mut info = egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tr(inp.label));
let mut info = egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tr(inp.label));
info.current_text_value = Some(prompt.to_string());
info
});
@@ -3196,13 +3196,13 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
{
d.refuse(app, error);
}
let shown_id = egui::Id::new("sc_dialog_refusal_shown");
let shown_id = egui::Id::unique("sc_dialog_refusal_shown");
if d.error.is_none() && app.preview.error.is_none() {
ui.data_mut(|m| m.remove::<String>(shown_id));
}
// A click the waiting pick field does not take: what that field is for, plainly (#10428).
// Under the fold in a short window, a new note scrolls into view once, as a refusal does.
let note_shown_id = egui::Id::new("sc_dialog_pick_refused_shown");
let note_shown_id = egui::Id::unique("sc_dialog_pick_refused_shown");
if let Some(id) = d.pick_refused.filter(|_| d.error.is_none() && app.preview.error.is_none()) {
let note = crate::ui_kit::text(ui, crate::i18n::tr(id), crate::ui_kit::Text::Hint("pick_refused"));
crate::scenario::publish_rect("dialog:pick_refused", note.rect);
@@ -3837,9 +3837,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
| Kind::Sheet(crate::dialogs_sheet::Sm::Rules { .. })
| Kind::Plastic(crate::dialogs_plastic::Pl::Rules { .. })
);
let grow_key = egui::Id::new(("sc_dialog_label_grow", raw_title));
let grow_key = egui::Id::unique(("sc_dialog_label_grow", raw_title));
let label_grow = ctx.data(|data| data.get_temp::<f32>(grow_key)).unwrap_or(0.0).min((vp.width() - t.space_24 - card_width(&d)).max(0.0));
let panel = crate::ui_kit::Panel::new(&heading, egui::Id::new(("sc_dialog", title(&d.kind))), anchor, drag_bounds, side, frame)
let panel = crate::ui_kit::Panel::new(&heading, egui::Id::unique(("sc_dialog", title(&d.kind))), anchor, drag_bounds, side, frame)
.actions(ok_label, close_label);
let panel = if let Some(column) = column {
panel.width(column.width()).joined(crate::ui_kit::DockEdges { left: true, top: true, ..Default::default() })
@@ -3859,7 +3859,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
matches!(&d.kind, Kind::SketchModify(k) if crate::sketch_modify_ui::repeatable(k)) && d.editing.is_none() && !d.extra.contains_key("pattern");
if let Kind::ConfirmDelete { with, fail, .. } = &d.kind {
let detail = with.join(" · ");
let (closed, action) = crate::ui_kit::Confirm::new(egui::Id::new(("sc_dialog", title(&d.kind))), "delete", &heading)
let (closed, action) = crate::ui_kit::Confirm::new(egui::Id::unique(("sc_dialog", title(&d.kind))), "delete", &heading)
.tone(crate::ui_kit::Tone::Danger)
.actions("Delete", "Cancel", None)
.detail(&detail)
@@ -3997,7 +3997,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
// Keyboard: Enter applies (from a value field, or with nothing focused), Esc cancels.
let (enter_free, esc) = ctx.input(|i| (i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape)));
let nothing_focused = ctx.memory(|m| m.focused().is_none());
let canvas_enter = enter_free && ctx.memory(|m| m.had_focus_last_frame(egui::Id::new("sc_canvas_value")));
let canvas_enter = enter_free && ctx.memory(|m| m.had_focus_last_frame(egui::Id::unique("sc_canvas_value")));
// Tables (configurations, a study's keys) take Enter for their cells.
let enter_applies = !matches!(d.kind, Kind::Sketch | Kind::Measure { .. } | Kind::ConfirmDelete { .. })
&& !matches!(&d.kind, Kind::Motion(k) if k.wide())
+2 -2
View File
@@ -485,7 +485,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
ui_kit::swatch_grid(ui, &items, |ui, (i, (look, n))| {
if ui_kit::color_swatch(
ui,
egui::Id::new(("ap_used", *i)),
egui::Id::unique(("ap_used", *i)),
c32(look.color, look.opacity),
k.color == look.color && k.opacity == look.opacity && k.pbr == look.pbr,
&format!("{} · {n} item(s)", look.name),
@@ -533,7 +533,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
|p| format!("{}\n{}\n{}", l.name, p.preview_note, p.requirements.join(", ")),
);
ui.add_enabled_ui(available, |ui| {
let response = ui_kit::color_swatch(ui, egui::Id::new(("ap_lib", *i)), c32(l.color, l.opacity), picked_look, &tip, (*l).clone());
let response = ui_kit::color_swatch(ui, egui::Id::unique(("ap_lib", *i)), c32(l.color, l.opacity), picked_look, &tip, (*l).clone());
if let Some(preset) = &l.preset {
crate::scenario::publish_handle(&format!("appearance:preset:{}", preset.id), response.rect.center());
}
+6 -6
View File
@@ -911,7 +911,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
form_row(ui, &capital(name), |ui| {
if i == 0 {
// Typing replaces the value at once, as in the other dialogs' value boxes.
let id = egui::Id::new("sc_drive_value");
let id = egui::Id::unique("sc_drive_value");
let r = crate::ui_kit::cell_editor(ui, id, v, "", false);
crate::params_dialog::complete(ui, &r, v);
if std::mem::take(focus) {
@@ -1837,7 +1837,7 @@ pub(crate) fn timeline_joint_tiles(app: &mut SolveApp, ui: &mut egui::Ui, p: &eg
for (id, name, kind, suppressed) in joints {
let br = egui::Rect::from_min_size(egui::pos2(x, r.top() + 6.0), vec2(28.0, 28.0));
crate::scenario::publish_handle(&format!("timeline-joint:{name}"), br.center());
let resp = ui.interact(br, ui.id().with(("joint", id)), egui::Sense::click());
let resp = ui.interact(br, ui.scope_id().with(("joint", id)), egui::Sense::click());
let bg = if app.tree.picked_joints.contains(&id) {
t.accent_soft
} else if resp.hovered() {
@@ -1961,7 +1961,7 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
let folder_shown = !app.session.folder_hidden(key);
let r = draw_row(
ui,
ui.id().with(key),
ui.scope_id().with(key),
&Row { depth, fold: Some(open), eye: Some(folder_shown), icon: "folder", label: "Joints", dim: !folder_shown, ..Default::default() },
);
if r.eye {
@@ -1978,7 +1978,7 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
let selected = app.tree.picked_joints.contains(&id);
let r = draw_row(
ui,
ui.id().with(("joint", id)),
ui.scope_id().with(("joint", id)),
&Row {
depth: depth + 1,
eye: Some(visible),
@@ -2050,7 +2050,7 @@ pub fn component_joints(app: &mut SolveApp, ui: &mut egui::Ui, path: &[u64], key
}
let key = format!("{key}/joints");
let open = is_open(app, &key, false);
let r = draw_row(ui, ui.id().with(&key), &Row { depth, fold: Some(open), icon: "folder", label: "Joints", ..Default::default() });
let r = draw_row(ui, ui.scope_id().with(&key), &Row { depth, fold: Some(open), icon: "folder", label: "Joints", ..Default::default() });
if r.fold || r.clicked {
toggle(app, &key, false);
}
@@ -2066,7 +2066,7 @@ pub fn component_joints(app: &mut SolveApp, ui: &mut egui::Ui, path: &[u64], key
let selected = app.tree.picked_joints.contains(&id);
let r = draw_row(
ui,
ui.id().with((&key, "joint", id)),
ui.scope_id().with((&key, "joint", id)),
&Row {
depth: depth + 1,
eye: Some(visible),
+1 -1
View File
@@ -873,7 +873,7 @@ fn slider_drag_does_not_open_suggestions_over_the_slider() {
.unwrap();
h.app.start("joint.drive");
h.frames(4);
let editor = egui::Id::new("sc_drive_value");
let editor = egui::Id::unique("sc_drive_value");
let bounds = h
.output
.accesskit_update
+2 -1
View File
@@ -466,7 +466,8 @@ mod tests {
(Dim::ThreadDepth, 145.0),
]
.map(|(dim, y)| (dim, Rect::from_min_size(pos2(150.0, y), vec2(100.0, 20.0)), false));
let output = ctx.run_ui(egui::RawInput::default(), |ui| paint(ui.painter(), pic, &section, &slots));
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| paint(ui.painter(), pic, &section, &slots));
output.textures_delta.clear();
let mut dimensions: Vec<_> = output
.shapes
.iter()
+7 -1
View File
@@ -218,7 +218,13 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
};
if let Some(look) = look {
let [r, g, b] = look.color;
ui_kit::color_thumbnail(ui, ui.id().with((&entry.id, "sample")), egui::Color32::from_rgb(r, g, b), &look.name, ());
ui_kit::color_thumbnail(
ui,
ui.scope_id().with((&entry.id, "sample")),
egui::Color32::from_rgb(r, g, b),
&look.name,
(),
);
}
let button =
ui_kit::list_picker(ui, &entry.id, entry.definition.name(), k.local.selected.as_deref() == Some(entry.id.as_str()))
+1 -1
View File
@@ -358,7 +358,7 @@ fn plot_rows(
if what != Plotted::Nothing {
choice_row(ui, "motion.plot_quantity", "ms_quantity", &mut a.quantity, &QUANTITIES);
form_row(ui, "Duration", |ui| {
let r = crate::ui_kit::cell_editor(ui, ui.id().with("duration"), &mut a.duration, "motion.duration_hint", false);
let r = crate::ui_kit::cell_editor(ui, ui.scope_id().with("duration"), &mut a.duration, "motion.duration_hint", false);
crate::scenario::publish_handle("dialog:Duration", r.rect.center());
});
let params = study.clone().and_then(|st| plot_params(s, what, &st, &a.duration, inputs).map(|p| (st, p)));
+1 -1
View File
@@ -225,7 +225,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
text_row(ui, "Name", &mut k.name, crate::i18n::tr("parts.automatic_name"));
// The part, and its table values.
let key = (k.family, k.size, k.length);
let tex_id = egui::Id::new("sc_part_picture");
let tex_id = egui::Id::unique("sc_part_picture");
if k.picture != Some(key) {
let tex = insert_params(&lib, k, &[])
.ok()
+2 -2
View File
@@ -261,7 +261,7 @@ pub fn start_with_format(app: &mut SolveApp, format: &str) {
/// A popup with the slicer list is open (Delete belongs to it, not to the design's selection).
pub fn popup_open(ctx: &egui::Context) -> bool {
let frame = ctx.cumulative_frame_nr();
let at = ctx.data(|d| d.get_temp::<u64>(egui::Id::new("sc_slicer_popup")));
let at = ctx.data(|d| d.get_temp::<u64>(egui::Id::unique("sc_slicer_popup")));
at.is_some_and(|n| n + 1 >= frame)
}
@@ -435,7 +435,7 @@ fn slicer_combo(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh) {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
let r = crate::ui_kit::dropdown_menu(ui, "sm_slicer", &text, |ui| {
let frame = ctx.cumulative_frame_nr();
ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_slicer_popup"), frame));
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_slicer_popup"), frame));
for e in &entries {
let tex = icon(&mut app.slicer_menu, &ctx, e);
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click());
+1 -1
View File
@@ -1050,7 +1050,7 @@ pub fn overlay(app: &mut SolveApp, painter: &egui::Painter, rect: Rect, d: &Dial
// to its width limit, so its actual edge, rather than a fixed reserve, bounds the view.
let panel_left = painter
.ctx()
.memory(|m| m.area_rect(egui::Id::new(("sc_dialog", "FLAT PATTERN"))))
.memory(|m| m.area_rect(egui::Id::unique(("sc_dialog", "FLAT PATTERN"))))
.map(|panel| panel.left())
.unwrap_or(rect.right() - crate::frame::DIALOG_MAX_W);
let content = crate::shell::content_rect(app, rect);
+19 -8
View File
@@ -173,7 +173,7 @@ fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
*data = Some(Ok(flat));
let ctx = crate::fonts::test_context();
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("flat-pocket-test")));
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::unique("flat-pocket-test")));
assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
});
let texts: Vec<_> = crate::test_support::text_shapes(&output).map(|text| text.galley.text()).collect();
@@ -251,7 +251,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
app.shell.content = Some(Rect::from_min_max(pos2(browser.right(), viewport.top()), pos2(viewport.right(), history.top())));
}
let ctx = crate::fonts::test_context();
let id = egui::Id::new(("sc_dialog", "FLAT PATTERN"));
let id = egui::Id::unique(("sc_dialog", "FLAT PATTERN"));
let mut output = egui::FullOutput::default();
for _ in 0..3 {
output =
@@ -262,7 +262,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
.show(ui.ctx(), |ui| {
ui.label("Flat size: 50 × 10 × 2.5 mm");
});
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Background, egui::Id::new("island-fit")));
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Background, egui::Id::unique("island-fit")));
assert!(overlay(
&mut app,
&painter,
@@ -314,7 +314,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
*data = Some(Ok(flat));
let ctx = crate::fonts::test_context();
let output = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("coincident-pocket-test")));
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::unique("coincident-pocket-test")));
assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
});
let labels: Vec<_> =
@@ -1049,8 +1049,13 @@ fn flat_pattern_pans_and_zooms_like_the_design_viewport() {
assert!(panned + zoomed >= 1, "{preset}: the preset has a pan or zoom drag");
// The wheel zooms (to the cursor when asked), identically.
let before = flat.flat_view.cam;
let wheel =
Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move };
let wheel = Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, 3.0),
modifiers: none,
phase: egui::TouchPhase::Move,
};
for (app, ctx, t) in [(&mut flat, &flat_ctx, &mut ft), (&mut design, &design_ctx, &mut dt)] {
frame(app, ctx, t, none, vec![Event::PointerMoved(at)]);
frame(app, ctx, t, none, vec![wheel.clone()]);
@@ -1109,7 +1114,13 @@ fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerButton { pos: at, button, pressed: false, modifiers: mods }]);
}
}
let wheel = Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move };
let wheel = Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, 3.0),
modifiers: none,
phase: egui::TouchPhase::Move,
};
frame(&mut app, &ctx, &mut t, none, vec![wheel]);
frame(&mut app, &ctx, &mut t, none, vec![Event::Zoom(1.3)]);
for _ in 0..30 {
@@ -1177,7 +1188,7 @@ fn flat_circles(app: &mut SolveApp, dialog: &Dialog, rect: Rect) -> Vec<egui::ep
let ctx = crate::fonts::test_context();
let input = egui::RawInput { screen_rect: Some(rect), ..Default::default() };
let output = crate::test_support::frame(&ctx, input, |ui| {
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("perforation-test")));
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::unique("perforation-test")));
assert!(overlay(app, &painter, rect, dialog));
});
output.shapes.into_iter().filter_map(|s| if let egui::Shape::Circle(c) = s.shape { Some(c) } else { None }).collect()
+12 -7
View File
@@ -983,11 +983,13 @@ mod contribution_tests {
let mut designation = original.to_string();
let mut custom = None;
let doc = Document::default();
let _ = ctx.run_ui(Default::default(), |ui| {
ctx.run_ui(Default::default(), |ui| {
egui::Grid::new("hole_catalogue").show(ui, |ui| {
hole_rows(ui, &doc, &mut designation, &mut custom, &mut None);
});
});
})
.textures_delta
.clear();
assert_eq!(designation, original);
}
}
@@ -1134,11 +1136,14 @@ mod contribution_tests {
let mut hole = original.to_string();
let mut custom = None;
let doc = Document::default();
let _ = context.run_ui(Default::default(), |ui| {
egui::Grid::new("authored_hole").show(ui, |ui| {
hole_rows(ui, &doc, &mut hole, &mut custom, &mut None);
});
});
context
.run_ui(Default::default(), |ui| {
egui::Grid::new("authored_hole").show(ui, |ui| {
hole_rows(ui, &doc, &mut hole, &mut custom, &mut None);
});
})
.textures_delta
.clear();
assert_eq!(hole, original);
}
}
+72 -68
View File
@@ -1014,7 +1014,7 @@ fn is_plain_value(text: &str) -> bool {
/// The cursor ends just after the name and the field keeps the keyboard.
fn insert_reference(ctx: &egui::Context, edit: &mut Edit, name: &str) {
edit.typed = true;
let id = egui::Id::new("sc_dim_edit");
let id = egui::Id::unique("sc_dim_edit");
let mut state = egui::TextEdit::load_state(ctx, id).unwrap_or_default();
let len = edit.expr.chars().count();
let (start, end) = if is_plain_value(&edit.expr) {
@@ -1120,7 +1120,7 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
let (done, escaped) = window::show(app, ctx, &mut e, at);
return settle(app, e, done, escaped);
}
let id = egui::Id::new("sc_dim_edit");
let id = egui::Id::unique("sc_dim_edit");
let mut done: Option<bool> = None;
let mut escaped = false;
// A type-first value sits beside its text, clear of the pointer and of the target a click
@@ -1129,74 +1129,78 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
let popup_width = drawable.map_or(220.0, |rect| (rect.width() - 8.0).clamp(24.0, 220.0));
let anchor = edit_anchor(at, e.live.is_some(), drawable, popup_width);
// Background: under Preferences and Report a bug (#9016), like canvas value boxes.
Container::new(ctx, Surface::Canvas).area(egui::Id::new("sc_dim_edit_area")).fixed_pos(anchor).order(egui::Order::Background).show(ctx, |ui| {
let frame = Container::popup_frame(ui.style()).fill(Tokens::get().dialog_body).inner_margin(Tokens::get().space_4);
Container::show_backing(ui, frame, |ui| {
crate::ui_kit::scope(ui, |ui| {
ui.set_max_width(popup_width);
crate::ui_kit::text(ui, &crate::i18n::format("dialog.dimension_name", &[("name", &e.param)]), crate::ui_kit::Text::Inline)
.backing_on_hover_text(crate::i18n::tr("dialog.dimension_insert_hint"));
if !e.owned {
// The whole value is selected before the field reads its state, every frame
// until the person takes it over, so typing replaces it (#11204). Only on the
// first frame was not enough: a frame without the keyboard collapses the
// field's selection to its caret, and the first digit went in beside the value.
let n = e.expr.chars().count();
let mut s = egui::text_edit::TextEditState::load(ui.ctx(), id).unwrap_or_default();
s.cursor.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
e.selection = s.cursor.char_range();
s.store(ui.ctx(), id);
// A key this frame acts on the whole value (a digit replaces it, Home puts the
// caret before it); from then on the caret is the person's.
let focused = ui.memory(|m| m.has_focus(id));
e.owned = focused && ui.input(|i| i.events.iter().any(|ev| matches!(ev, egui::Event::Key { pressed: true, .. })));
}
let r = crate::ui_kit::cell_editor(ui, id, &mut e.expr, "", false);
// A press into the field puts the caret where it landed; not the press that placed
// or pinned the value, which began before the box sat there.
let settled = e.shown == Some(anchor);
e.owned |= settled && r.hovered() && ui.input(|i| i.pointer.any_pressed());
crate::theme::TabAccent::number_focus(ui, &r);
if r.has_focus() {
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
}
let completed = crate::params_dialog::complete(ui, &r, &mut e.expr);
if completed {
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
}
if let Some(live) = e.live.as_mut() {
live.typed |= r.changed() || completed;
}
e.owned |= r.changed() || completed;
e.typed |= r.changed() || completed;
// Solved now, so the next frame draws the sketch at the new value from its start.
if (r.changed() || completed)
&& let Some(draft) = e.draft.as_mut()
{
let expr = e.expr.clone();
reevaluate(app, draft, &expr);
}
if let Some(error) = e.draft.as_ref().and_then(|d| d.error.as_ref()) {
crate::ui_kit::text(ui, error, crate::ui_kit::Text::Report("dimension_error", true));
}
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
if r.lost_focus() && enter {
done = Some(true);
} else if e.fresh > 0 {
if !r.has_focus() {
r.request_focus();
Container::new(ctx, Surface::Canvas).area(egui::Id::unique("sc_dim_edit_area")).fixed_pos(anchor).order(egui::Order::Background).show(
ctx,
|ui| {
let frame = Container::popup_frame(ui.style()).fill(Tokens::get().dialog_body).inner_margin(Tokens::get().space_4);
Container::show_backing(ui, frame, |ui| {
crate::ui_kit::scope(ui, |ui| {
ui.set_max_width(popup_width);
crate::ui_kit::text(ui, &crate::i18n::format("dialog.dimension_name", &[("name", &e.param)]), crate::ui_kit::Text::Inline)
.backing_on_hover_text(crate::i18n::tr("dialog.dimension_insert_hint"));
if !e.owned {
// The whole value is selected before the field reads its state, every frame
// until the person takes it over, so typing replaces it (#11204). Only on the
// first frame was not enough: a frame without the keyboard collapses the
// field's selection to its caret, and the first digit went in beside the value.
let n = e.expr.chars().count();
let mut s = egui::text_edit::TextEditState::load(ui.ctx(), id).unwrap_or_default();
s.cursor
.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
e.selection = s.cursor.char_range();
s.store(ui.ctx(), id);
// A key this frame acts on the whole value (a digit replaces it, Home puts the
// caret before it); from then on the caret is the person's.
let focused = ui.memory(|m| m.has_focus(id));
e.owned = focused && ui.input(|i| i.events.iter().any(|ev| matches!(ev, egui::Event::Key { pressed: true, .. })));
}
} else if r.lost_focus() {
done = Some(false);
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
done = Some(false);
// Esc cancels the whole dimension, picks included.
escaped = true;
}
let r = crate::ui_kit::cell_editor(ui, id, &mut e.expr, "", false);
// A press into the field puts the caret where it landed; not the press that placed
// or pinned the value, which began before the box sat there.
let settled = e.shown == Some(anchor);
e.owned |= settled && r.hovered() && ui.input(|i| i.pointer.any_pressed());
crate::theme::TabAccent::number_focus(ui, &r);
if r.has_focus() {
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
}
let completed = crate::params_dialog::complete(ui, &r, &mut e.expr);
if completed {
e.selection = egui::TextEdit::load_state(ctx, id).and_then(|s| s.cursor.char_range());
}
if let Some(live) = e.live.as_mut() {
live.typed |= r.changed() || completed;
}
e.owned |= r.changed() || completed;
e.typed |= r.changed() || completed;
// Solved now, so the next frame draws the sketch at the new value from its start.
if (r.changed() || completed)
&& let Some(draft) = e.draft.as_mut()
{
let expr = e.expr.clone();
reevaluate(app, draft, &expr);
}
if let Some(error) = e.draft.as_ref().and_then(|d| d.error.as_ref()) {
crate::ui_kit::text(ui, error, crate::ui_kit::Text::Report("dimension_error", true));
}
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
if r.lost_focus() && enter {
done = Some(true);
} else if e.fresh > 0 {
if !r.has_focus() {
r.request_focus();
}
} else if r.lost_focus() {
done = Some(false);
}
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
done = Some(false);
// Esc cancels the whole dimension, picks included.
escaped = true;
}
});
});
});
});
},
);
e.shown = Some(anchor);
settle(app, e, done, escaped);
}
+5 -5
View File
@@ -101,7 +101,7 @@ thread_local! {
/// Whether the open window is under `p`: the wheel there steps its value, so the camera under it
/// does not zoom (navigation asks).
pub(crate) fn covers(ctx: &egui::Context, p: Pos2) -> bool {
super::editing() && ctx.memory(|m| m.area_rect(egui::Id::new(AREA))).is_some_and(|r| r.contains(p))
super::editing() && ctx.memory(|m| m.area_rect(egui::Id::unique(AREA))).is_some_and(|r| r.contains(p))
}
/// Forget the worked-out value (the edit closed).
@@ -183,13 +183,13 @@ fn place(app: &SolveApp, ctx: &egui::Context, param: &str, at: Pos2, size: egui:
/// the control channel. True when clicked.
fn button(ui: &mut egui::Ui, rect: Rect, key: &str, tip: &str, icon: &str) -> bool {
let t = Tokens::get();
let response = ui.interact(rect, ui.id().with(key), egui::Sense::click());
let response = ui.interact(rect, ui.scope_id().with(key), egui::Sense::click());
if response.hovered() {
ui.painter().rect_filled(rect, t.control_radius, t.hover);
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
crate::icons::paint_icon(ui.painter(), rect.shrink(t.space_4), icon, 1.0);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tip));
crate::scenario::publish_target(key, rect);
response.on_hover_text(tip).clicked()
}
@@ -199,8 +199,8 @@ fn button(ui: &mut egui::Ui, rect: Rect, key: &str, tip: &str, icon: &str) -> bo
/// whether a new dimension's picks go too (the cross, Esc), as Esc in the box does.
pub(super) fn show(app: &mut SolveApp, ctx: &egui::Context, e: &mut Edit, at: Pos2) -> (Option<bool>, bool) {
let t = Tokens::get();
let id = egui::Id::new("sc_dim_edit");
let area = egui::Id::new(AREA);
let id = egui::Id::unique("sc_dim_edit");
let area = egui::Id::unique(AREA);
let (kind, error) = match &e.draft {
Some(draft) => (kind(&draft.preview_doc, &e.param), draft.error.clone()),
None => (kind(&app.session.doc, &e.param), existing(app, &e.param, &e.expr).1),
+12 -12
View File
@@ -23,7 +23,7 @@ fn place(h: &mut Harness, picks: &[[f64; 2]], at: [f64; 2]) {
}
h.step(&json!({"click":{"world":[at[0],at[1],0]}})).unwrap();
assert!(crate::dim_view::editing(), "placement opens an editor");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
}
fn type_value(h: &mut Harness, text: &str) {
h.step(&json!({"key":"A","cmd":true})).unwrap();
@@ -85,7 +85,7 @@ fn click_d1_and_apply(h: &mut Harness) -> String {
h.step(&json!({"click":{"dimension":"d1"}})).unwrap();
assert!(crate::dim_view::editing(), "a reference click keeps the editor open");
h.frames(2);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")), "the value field keeps the keyboard");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")), "the value field keeps the keyboard");
h.step(&json!({"key":"Enter"})).unwrap();
assert!(!crate::dim_view::editing(), "{}", h.call("ui.inspect", json!({})));
let expr = h.app.session.doc.param("d2").unwrap().expr.clone();
@@ -165,7 +165,7 @@ fn dimension_tool_click_on_an_existing_dimension_edits_its_value() {
h.step(&json!({"click":{"dimension":"d1"}})).unwrap();
assert!(crate::dim_view::editing(), "a click on the dimension opens its value");
h.frames(5);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")), "the value field takes the keyboard");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")), "the value field takes the keyboard");
type_value(&mut h, "25");
h.step(&json!({"key":"Enter"})).unwrap();
assert!(!crate::dim_view::editing(), "{}", h.call("ui.inspect", json!({})));
@@ -221,7 +221,7 @@ fn a_new_dimension_in_the_window_steps_previews_and_applies_once() {
h.step(&json!({"click":{"world":[10,6,0]}})).unwrap();
h.frames(3);
assert!(crate::dim_view::editing(), "placing the text opens the window");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")), "its field takes the keyboard");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")), "its field takes the keyboard");
let window = crate::scenario::rect_of("dimwin").expect("the window is shown");
let drawable = h.app.viewport.layout.as_ref().unwrap().drawable;
assert!(drawable.contains_rect(window), "the window {window:?} stays in the view {drawable:?}");
@@ -238,7 +238,7 @@ fn a_new_dimension_in_the_window_steps_previews_and_applies_once() {
assert!(crate::scenario::count_of("dimwin:preview_curves").unwrap_or(0.0) > 0.0, "what the value moves is drawn");
assert_eq!(h.app.session.doc.to_json(), original, "nothing reaches the design before OK");
assert_eq!(h.app.session.undo.len(), undo);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")), "the arrows leave the keyboard with the field");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")), "the arrows leave the keyboard with the field");
// The wheel over the field steps it instead of zooming the view.
let cam = h.app.cam;
let field = crate::scenario::rect_of("dimwin:value").unwrap();
@@ -922,7 +922,7 @@ fn dimension_count(h: &Harness) -> usize {
fn assert_live(h: &Harness) {
assert!(crate::dim_view::editing(), "the pick opens the value at once");
assert_eq!(crate::dim_view::live(), Some(false));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
}
#[test]
@@ -1298,7 +1298,7 @@ fn type_first_keeps_the_place_first_flow() {
h.step(&json!({"click":{"world":[10,-7,0]}})).unwrap();
assert!(crate::dim_view::editing());
assert_eq!(crate::dim_view::live(), None, "the text is pinned");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
h.step(&json!({"move":{"world":[0,12,0]}})).unwrap();
keys(&mut h, "18");
h.step(&json!({"key":"Enter"})).unwrap();
@@ -1321,7 +1321,7 @@ fn type_first_keeps_the_place_first_flow() {
assert_eq!(h.app.tool.as_ref().map(|t| t.picks.len()), Some(1));
h.step(&json!({"click":{"world":[10,6,0]}})).unwrap();
assert!(crate::dim_view::editing());
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
keys(&mut h, "21");
h.step(&json!({"key":"Enter"})).unwrap();
let new = added(&h, dims);
@@ -1332,7 +1332,7 @@ fn type_first_keeps_the_place_first_flow() {
/// Put the value field's caret at `at` with nothing selected, as the field itself does when it
/// misses the keyboard for a frame (a WebView2 showed the caret before the value, #11204).
fn caret_without_the_person(h: &Harness, at: usize) {
let id = egui::Id::new("sc_dim_edit");
let id = egui::Id::unique("sc_dim_edit");
let mut state = egui::TextEdit::load_state(&h.ctx, id).unwrap_or_default();
state.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(at))));
state.store(&h.ctx, id);
@@ -1352,7 +1352,7 @@ fn held_click(h: &mut Harness, at: [f64; 2], frames: usize) {
/// The value field's caret and selection end (characters).
fn caret(h: &Harness) -> Option<(usize, usize)> {
egui::TextEdit::load_state(&h.ctx, egui::Id::new("sc_dim_edit"))
egui::TextEdit::load_state(&h.ctx, egui::Id::unique("sc_dim_edit"))
.and_then(|s| s.cursor.char_range())
.map(|r| (r.primary.index.0, r.secondary.index.0))
}
@@ -1373,7 +1373,7 @@ fn typed_digits_replace_the_proposed_value_whatever_moved_the_caret() {
h.frames(12);
caret_without_the_person(&h, 0);
h.frames(2);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")), "held {held}");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")), "held {held}");
keys(&mut h, "400");
assert_eq!(crate::dim_view::draft_preview().map(|d| d.0).as_deref(), Some("400"), "held {held}");
h.step(&json!({"key":"Enter"})).unwrap();
@@ -1824,7 +1824,7 @@ fn double_clicking_a_shown_dimension_edits_it_without_editing_the_sketch() {
let at = model_text(&h, "d2");
h.step(&json!({"click":at,"double":true})).unwrap();
assert!(crate::dim_view::editing(), "the double-click opens the value");
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
assert_eq!(h.app.session.active_sketch, None, "the sketch is not edited");
type_value(&mut h, "14");
h.step(&json!({"key":"Enter"})).unwrap();
+10 -8
View File
@@ -943,7 +943,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
.collect();
let fit_all = shown == n && natural.iter().sum::<f32>() <= x1 - x0 - 34.0;
// A right-click on a tab opens its menu (#3055): the tab and where the menu opens.
let menu_id = egui::Id::new("sc_doc_tab_menu");
let menu_id = egui::Id::unique("sc_doc_tab_menu");
let mut menu: Option<(usize, egui::Pos2)> = ui.ctx().data(|d| d.get_temp(menu_id));
// The shown tabs' widths, in strip order.
let widths: Vec<f32> = (first..first + shown).map(|i| if fit_all { natural.get(i).copied().unwrap_or(w) } else { w }).collect();
@@ -952,7 +952,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
let title_bar = app.custom_titlebar || app.integrated_titlebar;
// A tab being dragged to a new place follows the pointer and the others make room for it;
// Escape puts it back.
let drag_id = egui::Id::new("sc_doc_tab_drag");
let drag_id = egui::Id::unique("sc_doc_tab_drag");
let (held_down, released, escape) = ui.input(|i| (i.pointer.primary_down(), i.pointer.primary_released(), i.key_pressed(egui::Key::Escape)));
let mut drag =
ui.ctx().data(|d| d.get_temp::<TabDrag>(drag_id)).filter(|d| (held_down || released) && !escape && (first..first + shown).contains(&d.from));
@@ -985,7 +985,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
};
let tr = Rect::from_min_size(pos2(left, r.top() + 4.0), vec2(w - 2.0, r.height() - 8.0));
let active = !app.home.open && (i == app.docs.active || app.docs.slots.is_empty());
let resp = ui.interact(tr, ui.id().with(("doc_tab", i)), Sense::click_and_drag());
let resp = ui.interact(tr, ui.scope_id().with(("doc_tab", i)), Sense::click_and_drag());
crate::scenario::publish_target(&format!("tab:design:{i}"), tr);
// Dragging a tab along the strip moves it. The tabs are also part of the title bar:
// dragging the only tab, or one mostly up or down, moves the window, and a double click
@@ -1028,7 +1028,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
// ×
let xr = Rect::from_center_size(pos2(tr.right() - 13.0, tr.center().y), vec2(16.0, 16.0));
let show_close = active || resp.hovered() || ui.rect_contains_pointer(tr);
let xresp = ui.interact(xr, ui.id().with(("doc_close", i)), if show_close { Sense::click() } else { Sense::hover() });
let xresp = ui.interact(xr, ui.scope_id().with(("doc_close", i)), if show_close { Sense::click() } else { Sense::hover() });
crate::scenario::publish_target(&format!("tab:close:{i}"), xr);
if show_close && (xresp.hovered() || xresp.has_focus()) {
ui.painter().rect_filled(xr, t.radius_sm.saturating_sub(1), t.app_bar_hover_strong);
@@ -1044,6 +1044,8 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
if resp.secondary_clicked() {
menu = Some((i, ui.ctx().pointer_interact_pos().unwrap_or(tr.left_bottom())));
}
// Named apart from a dialog's Close: egui 0.37 names an unnamed widget after its tooltip.
xresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, show_close, crate::i18n::tr("ui.close_tab")));
if xresp.backing_on_hover_text("Close").clicked() {
shut = Some(i);
} else if (if held.is_some() { resp } else { resp.backing_on_hover_text(tip) }).clicked() {
@@ -1060,7 +1062,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
// More tabs than fit: a chevron lists them all.
if shown < n {
let mr = Rect::from_center_size(pos2(x + 12.0, r.center().y), vec2(20.0, 22.0));
let mresp = ui.interact(mr, ui.id().with("doc_more"), Sense::click());
let mresp = ui.interact(mr, ui.scope_id().with("doc_more"), Sense::click());
crate::scenario::publish_handle("tab:overflow", mr.center());
if mresp.hovered() {
ui.painter().rect_filled(mr, t.radius_sm, t.app_bar_hover_button);
@@ -1072,7 +1074,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
FontId::proportional(crate::theme::Tokens::get().font_body),
t.app_bar_text,
);
let id = egui::Id::new("sc_doc_more");
let id = egui::Id::unique("sc_doc_more");
if mresp.clone().backing_on_hover_text(crate::i18n::tr_label("All open designs")).clicked() {
ui.ctx().data_mut(|d| d.insert_temp(id, !d.get_temp::<bool>(id).unwrap_or(false)));
}
@@ -1142,7 +1144,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
});
// Choose a new design, open or sample from the File view before creating a tab.
let pr = Rect::from_center_size(pos2(x + 14.0, r.center().y), vec2(22.0, 22.0));
let presp = ui.interact(pr, ui.id().with("doc_new"), Sense::click());
let presp = ui.interact(pr, ui.scope_id().with("doc_new"), Sense::click());
if presp.hovered() {
ui.painter().rect_filled(pr, t.radius_sm, t.app_bar_hover_button);
}
@@ -1260,7 +1262,7 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
answer = Some(choice);
}
let title = crate::i18n::format("document.save_changes", &[("name", &name_display)]);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_close_prompt"), "status_warning", &title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_close_prompt"), "status_warning", &title)
.tone(ui_kit::Tone::Warning)
.actions(crate::i18n::tr("ui.save"), crate::i18n::tr("ui.cancel"), Some(crate::i18n::tr("ui.don_t_save")))
.secondary_left()
+6 -5
View File
@@ -108,11 +108,12 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let Some(params) = app.easyprint.ask.clone() else { return };
let _language = crate::i18n::scope(&app.preferences.language);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_easyprint_confirm"), "mesh_export", crate::i18n::tr_label("Open with EasyPrint"))
.actions("Upload", "Cancel", None)
.show(ctx, true, |ui| {
ui_kit::text(ui, crate::i18n::tr("ui.easyprint_consent"), ui_kit::Text::Report("easyprint_consent", false));
});
let (closed, action) =
ui_kit::Confirm::new(egui::Id::unique("sc_easyprint_confirm"), "mesh_export", crate::i18n::tr_label("Open with EasyPrint"))
.actions("Upload", "Cancel", None)
.show(ctx, true, |ui| {
ui_kit::text(ui, crate::i18n::tr("ui.easyprint_consent"), ui_kit::Text::Report("easyprint_consent", false));
});
match action {
ui_kit::Action::Ok => {
app.easyprint.ask = None;
+1 -1
View File
@@ -50,7 +50,7 @@ fn start(h: &Harness, id: &str) -> Vec2 {
}
fn clipboard_frame(h: &mut Harness, ctx: &egui::Context, events: Vec<egui::Event>) {
let _ = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| super::handle(&mut h.app, ui.ctx()));
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| super::handle(&mut h.app, ui.ctx())).textures_delta.clear();
}
fn middle_button(pressed: bool) -> egui::Event {
+3 -3
View File
@@ -43,7 +43,7 @@ struct Popup {
}
fn popup_id() -> egui::Id {
egui::Id::new("expression_completion_keyboard")
egui::Id::unique("expression_completion_keyboard")
}
/// Run before any TextEdit or viewport shortcuts. Only the focused, recently drawn popup
@@ -322,7 +322,7 @@ mod tests {
app.run("parameters.add", json!({"name":name,"expression":format!("{value} mm")})).unwrap();
}
params_dialog::publish(&mut app, &ctx);
let id = egui::Id::new("completion_test_editor");
let id = egui::Id::unique("completion_test_editor");
ctx.memory_mut(|m| m.request_focus(id));
let mut editor = Self { ctx, app, text: text.into(), id, accepted: false, leaked: Vec::new() };
editor.frame(Vec::new());
@@ -399,7 +399,7 @@ mod tests {
let long = "Driver_MountHole_to_Cover_Shared_Base_Layout_Angle";
app.run("parameters.add", json!({"name": long, "expression": "8 deg"})).unwrap();
params_dialog::publish(&mut app, &ctx);
let id = egui::Id::new("long_name_completion");
let id = egui::Id::unique("long_name_completion");
ctx.memory_mut(|m| m.request_focus(id));
let mut text = "Driver".to_string();
let mut output = ctx.run_ui(
+4 -4
View File
@@ -378,7 +378,7 @@ fn rotated_second_occurrence_keeps_saved_from_and_to_points_exact() {
fn dragging_to_update_value_reselects_whole_value_box_text() {
let mut h = make(json!({"distance":"4 mm"}));
open(&mut h);
let id = egui::Id::new("sc_canvas_value");
let id = egui::Id::unique("sc_canvas_value");
let selection = |h: &Harness| {
let range = egui::text_edit::TextEditState::load(&h.ctx, id).unwrap().cursor.char_range().unwrap();
let (a, b) = (range.primary.index, range.secondary.index);
@@ -604,15 +604,15 @@ fn without_value_boxes_in_the_view_the_distance_is_typed_in_the_window() {
let mut h = make(json!({"distance":"4 mm"}));
h.app.preferences.value_boxes_in_view = false;
open(&mut h);
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).is_none(), "a value box in the view");
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).is_none(), "a value box in the view");
let focused = h.ctx.memory(|m| m.focused());
assert!(focused.is_some() && focused != Some(egui::Id::new("sc_canvas_value")), "{focused:?}");
assert!(focused.is_some() && focused != Some(egui::Id::unique("sc_canvas_value")), "{focused:?}");
h.step(&json!({"text":"25 mm"})).unwrap();
h.frames(2);
assert!((dialog_distance(&h) - 25.0).abs() < 1e-9, "{}", dialog_distance(&h));
h.step(&json!({"drag":[{"handle":"arrow"},on_arrow(&h, 30.0)]})).unwrap();
assert!((dialog_distance(&h) - 30.0).abs() < 0.5, "{}", dialog_distance(&h));
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).is_none(), "a value box in the view");
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).is_none(), "a value box in the view");
}
#[test]
+2 -2
View File
@@ -1261,14 +1261,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
m.surrender_focus(id);
}
});
let id = egui::Id::new("file_loading");
let id = egui::Id::unique("file_loading");
let card_id = id.with("card");
// Above the dialogs over the design; menus, tooltips and the resize edges stay above it.
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, id));
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, card_id));
Container::new(ctx, Surface::InputShield).area(id).order(egui::Order::Middle).fixed_pos(cover.min).interactable(true).show(ctx, |ui| {
ui.set_min_size(cover.size());
ui.interact(cover, egui::Id::new("file_loading_block"), egui::Sense::click_and_drag());
ui.interact(cover, egui::Id::unique("file_loading_block"), egui::Sense::click_and_drag());
let shield = Container::new(ctx, Surface::InputShield);
shield.paint_background(ui.painter(), shield.frame(ctx, egui::Color32::from_black_alpha(70), crate::frame::Side::Left), cover);
});
+2 -1
View File
@@ -527,10 +527,11 @@ mod tests {
_ => false,
}
}
let out = h.ctx.run_ui(
let mut out = h.ctx.run_ui(
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))), ..Default::default() },
|ui| h.app.ui(ui),
);
out.textures_delta.clear();
out.shapes.iter().any(|shape| contains(&shape.shape, hint))
}
+5 -5
View File
@@ -84,7 +84,7 @@ impl RichTextExt for RichText {
// Keep pending definitions too: set_fonts takes effect on the next frame, while the web
// fetch and preference update can both finish before that frame starts.
fn installed_fonts_id() -> egui::Id {
egui::Id::new("solidcraft_interface_fonts")
egui::Id::unique("solidcraft_interface_fonts")
}
fn install(ctx: &egui::Context, fonts: FontDefinitions) {
@@ -1089,7 +1089,7 @@ mod tests {
fonts.families[&egui::FontFamily::Proportional]
.iter()
.filter_map(|name| fonts.font_data.get(name))
.any(|data| ttf_parser::Face::parse(&data.font, data.index).unwrap().glyph_index(c).is_some())
.any(|data| ttf_parser::Face::parse(data.bytes(), data.index).unwrap().glyph_index(c).is_some())
}
/// The symbols the built-in Latin faces lack draw through the built-in Hack (#2955), which
@@ -1146,14 +1146,14 @@ mod tests {
assert_eq!(list[0], name);
assert_eq!(&list[1..], defaults.families[&FontFamily::Proportional]);
let data = &fonts.font_data[name];
assert_eq!(data.font.as_ref(), PLEX_SANS);
assert_eq!(data.bytes(), PLEX_SANS);
assert_eq!(data.tweak.coords.as_ref()[0].1, weight);
assert!(data.variation_axes().iter().any(|a| a.tag == *b"wght"));
}
let mono = &fonts.families[&FontFamily::Monospace];
assert_eq!(mono[0], "plex-mono");
assert_eq!(&mono[1..], defaults.families[&FontFamily::Monospace]);
assert_eq!(fonts.font_data["plex-mono"].font.as_ref(), PLEX_MONO);
assert_eq!(fonts.font_data["plex-mono"].bytes(), PLEX_MONO);
}
#[test]
@@ -1239,7 +1239,7 @@ mod tests {
fn latin_font_bytes() -> Vec<u8> {
let defaults = base_font_definitions();
let name = &defaults.families[&egui::FontFamily::Proportional][0];
defaults.font_data[name].font.to_vec()
defaults.font_data[name].bytes().to_vec()
}
#[test]
+9 -9
View File
@@ -33,14 +33,14 @@ impl Side {
/// How far a docked panel with this key was dragged.
pub fn dock_offset(ctx: &egui::Context, key: &str) -> egui::Vec2 {
ctx.data(|d| d.get_temp::<egui::Vec2>(egui::Id::new(("sc_dock_offset", key)))).unwrap_or(egui::Vec2::ZERO)
ctx.data(|d| d.get_temp::<egui::Vec2>(egui::Id::unique(("sc_dock_offset", key)))).unwrap_or(egui::Vec2::ZERO)
}
/// A title drag owns the pointer even when it outruns the dialog's previous position.
pub(crate) fn dock_dragging(ctx: &egui::Context) -> bool {
let frame = ctx.cumulative_frame_nr();
// The viewport can draw before the dialog, so retain the preceding frame's marker.
ctx.data(|d| d.get_temp::<u64>(egui::Id::new("sc_dock_drag_frame"))).is_some_and(|at| at >= frame.saturating_sub(1))
ctx.data(|d| d.get_temp::<u64>(egui::Id::unique("sc_dock_drag_frame"))).is_some_and(|at| at >= frame.saturating_sub(1))
&& ctx.input(|i| i.pointer.any_down())
}
@@ -62,14 +62,14 @@ pub fn offset_anchor(docked: Pos2, offset: egui::Vec2, vp: egui::Rect, side: Sid
/// it on screen). Called before the band's buttons are added, so they keep their clicks.
pub fn drag_dock(ui: &Ui, band: egui::Rect, key: &str, moved: egui::Vec2, applied: egui::Vec2) {
crate::scenario::publish_handle("dialog:title", band.center());
let response = ui.interact(band, ui.id().with("sc_dock_drag"), egui::Sense::drag());
let response = ui.interact(band, ui.scope_id().with("sc_dock_drag"), egui::Sense::drag());
if response.dragged() {
let frame = ui.ctx().cumulative_frame_nr();
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("sc_dock_drag_frame"), frame));
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique("sc_dock_drag_frame"), frame));
}
// The drag runs from where the press started, so no movement is lost while egui decides
// it is a drag, nor on the frame it ends.
let base = egui::Id::new("sc_dock_drag_base");
let base = egui::Id::unique("sc_dock_drag_base");
if response.drag_started()
&& let Some(origin) = ui.input(|i| i.pointer.press_origin())
{
@@ -82,12 +82,12 @@ pub fn drag_dock(ui: &Ui, band: egui::Rect, key: &str, moved: egui::Vec2, applie
(Some((start, from)), Some(now)) => start + (now - from),
_ => applied + response.drag_delta(),
};
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new(("sc_dock_offset", key)), to));
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique(("sc_dock_offset", key)), to));
} else if response.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
} else if moved != applied {
// Kept on screen: remember where it is, not where it was dragged off to.
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new(("sc_dock_offset", key)), applied));
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::unique(("sc_dock_offset", key)), applied));
}
}
@@ -174,7 +174,7 @@ mod tests {
h.step(&json!({"start": "parameters.change"})).unwrap();
h.frames(6);
let title = crate::scenario::handle_at("dialog:title").unwrap();
assert!(!h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_params_dialog"))).unwrap().contains(p));
assert!(!h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_params_dialog"))).unwrap().contains(p));
h.call("ui.move", json!({"x": title.x, "y": title.y}));
h.app.synthetic.push(egui::Event::PointerButton {
pos: title,
@@ -186,7 +186,7 @@ mod tests {
h.app.synthetic.push(egui::Event::PointerMoved(p));
h.frame();
assert!(h.ctx.dragged_id().is_some(), "the title must own the drag");
assert!(!h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_params_dialog"))).unwrap().contains(p), "the pointer must outrun the dialog");
assert!(!h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_params_dialog"))).unwrap().contains(p), "the pointer must outrun the dialog");
assert!(h.app.viewport.hover.is_none(), "dragging a dialog must block model highlights");
assert!(h.app.viewport.mouse.is_none(), "the viewport must not receive the drag pointer");
h.frames(2);
+1 -1
View File
@@ -192,7 +192,7 @@ pub(crate) fn paint(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter,
{
r = r.translate(vec2(0.0, nav.top() - 6.0 - r.bottom()));
}
let id = egui::Id::new("sc_frame_rate");
let id = egui::Id::unique("sc_frame_rate");
let clicked = ui.interact(r, id, Sense::click()).backing_on_hover_text(tr("perf.click_for_details")).clicked();
if clicked {
app.preferences.frame_rate_details = !app.preferences.frame_rate_details;
+6 -4
View File
@@ -64,7 +64,7 @@ pub(crate) fn paint_background_picture(app: &mut crate::SolveApp, painter: &egui
return;
}
let ctx = painter.ctx().clone();
let id = egui::Id::new("sc_background_picture");
let id = egui::Id::unique("sc_background_picture");
let mut picture = ctx.data(|d| d.get_temp::<BackgroundPicture>(id)).filter(|p| p.path == *path).unwrap_or_else(|| {
let picture = BackgroundPicture { path: path.clone(), decoded: Default::default(), texture: None };
let (path, slot, repaint) = (path.clone(), picture.decoded.clone(), ctx.clone());
@@ -829,7 +829,7 @@ mod tests {
time += 0.023;
let mut raw = egui::RawInput { time: Some(time), ..Default::default() };
raw.viewports.entry(egui::ViewportId::ROOT).or_default().occluded = Some(!visible);
let _ = ctx.run_ui(raw, |ui| {
ctx.run_ui(raw, |ui| {
ui.ctx().request_repaint(); // Deterministic harness only; product never does this for sampling.
let paint = rendered.then(|| crate::blur::PaintedFrame {
frame: ui.ctx().cumulative_frame_nr().saturating_sub(1),
@@ -838,7 +838,9 @@ mod tests {
});
session.sample(ui.ctx(), revision, radius, available, paint);
session.sample(ui.ctx(), revision, radius, available, paint); // egui re-pass duplicate
});
})
.textures_delta
.clear();
};
feed(&mut session, false, true, true, true, 1, 40.0);
feed(&mut session, false, true, true, true, 1, 40.0);
@@ -1250,7 +1252,7 @@ mod tests {
.is_some_and(|shape| matches!(shape, egui::Shape::Rect(_))),
"backing precedes native popup chrome"
);
assert!(ctx.memory(|m| m.has_focus(egui::Id::new("blur_picker").with("search"))), "native search focus survives hover");
assert!(ctx.memory(|m| m.has_focus(egui::Id::unique("blur_picker").with("search"))), "native search focus survives hover");
}
}
+5 -5
View File
@@ -106,7 +106,7 @@ fn connect(app: &mut SolveApp, ctx: &egui::Context) {
let Some(lines) = app.help.connect.clone() else {
return;
};
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_connect_assistants"), "help", crate::i18n::tr_label("Connect AI assistants"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_connect_assistants"), "help", crate::i18n::tr_label("Connect AI assistants"))
.cards(2.0)
.actions("", "Close", None)
.show(ctx, true, |ui| {
@@ -127,7 +127,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
if app.help.menu {
let mut pick: Option<&str> = None;
let resp = Container::new(ctx, Surface::Popup)
.area(egui::Id::new("sc_help_menu"))
.area(egui::Id::unique("sc_help_menu"))
.fixed_pos(anchor)
.constrain(true)
.order(egui::Order::Foreground)
@@ -165,11 +165,11 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
run(app, ctx, p);
} else if ctx.input(|i| i.pointer.any_pressed() || i.key_pressed(egui::Key::Escape)) && !resp.response.contains_pointer() {
// Pressed elsewhere (the ? button toggles it itself).
if !ctx.data(|d| d.get_temp::<bool>(egui::Id::new("sc_help_opened")).unwrap_or(false)) {
if !ctx.data(|d| d.get_temp::<bool>(egui::Id::unique("sc_help_opened")).unwrap_or(false)) {
app.help.menu = false;
}
}
ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_help_opened"), false));
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_help_opened"), false));
}
about(app, ctx);
connect(app, ctx);
@@ -181,7 +181,7 @@ fn about(app: &mut SolveApp, ctx: &egui::Context) {
if !app.help.about {
return;
}
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_about"), "help", crate::i18n::tr_label("About SolidCraft"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_about"), "help", crate::i18n::tr_label("About SolidCraft"))
.cards(2.0)
.actions("", "Close", None)
.show(ctx, true, |ui| {
+1 -1
View File
@@ -278,7 +278,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painte
let mut removed: Option<usize> = None;
for (n, (i, at)) in places.into_iter().enumerate() {
let Some(mut c) = proj.to_screen(at) else { continue };
let id = egui::Id::new(("sc_hole_place", i));
let id = egui::Id::unique(("sc_hole_place", i));
let r = ui.interact(egui::Rect::from_center_size(c, vec2(2.0 * HANDLE_REACH, 2.0 * HANDLE_REACH)), id, egui::Sense::click_and_drag());
// A click on the hole takes it out, as a pick of a picked face does.
if r.clicked() {
+7 -5
View File
@@ -422,7 +422,7 @@ pub(crate) fn rows(ui: &mut egui::Ui, d: &mut Dialog, library: &mut Library, doc
if !matches!(d.kind, Kind::Hole { .. }) {
return;
}
let id = egui::Id::new("personal_hole_presets_panel");
let id = egui::Id::unique("personal_hole_presets_panel");
let mut state = if reset { PanelState::default() } else { ui.ctx().data_mut(|data| data.get_temp::<PanelState>(id)).unwrap_or_default() };
ui_kit::section(ui, "personal_hole_presets", "hole_presets.title", false, |ui| {
ui.label(tr("hole_presets.scope"));
@@ -454,7 +454,7 @@ pub(crate) fn rows(ui: &mut egui::Ui, d: &mut Dialog, library: &mut Library, doc
}
});
response.widget_info(|| {
let mut info = egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, ui.is_enabled(), tr("hole_presets.preset"));
let mut info = egui::WidgetInfo::labeled(egui::Role::ComboBox, ui.is_enabled(), tr("hole_presets.preset"));
info.current_text_value = Some(display.clone());
info
});
@@ -521,7 +521,9 @@ pub(crate) fn rows(ui: &mut egui::Ui, d: &mut Dialog, library: &mut Library, doc
/// when Escape has already blurred its text field. Stale state cannot block other tools.
pub(crate) fn name_prompt_open(ctx: &egui::Context, dialog: Option<&Dialog>) -> bool {
dialog.is_some_and(|d| matches!(&d.kind, Kind::Hole { .. }))
&& ctx.data_mut(|data| data.get_temp::<PanelState>(egui::Id::new("personal_hole_presets_panel"))).is_some_and(|state| state.prompt.is_some())
&& ctx
.data_mut(|data| data.get_temp::<PanelState>(egui::Id::unique("personal_hole_presets_panel")))
.is_some_and(|state| state.prompt.is_some())
}
/// Draw after the Hole card so the modal owns input and its footer handles. `true` also
@@ -530,11 +532,11 @@ pub(crate) fn name_prompt(ctx: &egui::Context, d: &mut Dialog, library: &mut Lib
if !matches!(d.kind, Kind::Hole { .. }) {
return false;
}
let id = egui::Id::new("personal_hole_presets_panel");
let id = egui::Id::unique("personal_hole_presets_panel");
let mut state = ctx.data_mut(|data| data.get_temp::<PanelState>(id)).unwrap_or_default();
let Some(mut prompt) = state.prompt.take() else { return false };
let title = tr(if prompt.renaming { "hole_presets.rename" } else { "hole_presets.save" });
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("personal_hole_preset_name"), "save", title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("personal_hole_preset_name"), "save", title)
.actions(if prompt.renaming { tr("hole_presets.rename") } else { crate::i18n::tr_label("Save") }, "Cancel", None)
.show(ctx, true, |ui| {
ui_kit::row(ui, "hole_presets.name", |ui| ui_kit::focused_text_field(ui, &mut prompt.name, tr("hole_presets.name_hint")));
+1 -1
View File
@@ -413,7 +413,7 @@ pub(crate) fn canvas(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter,
let at = box_beyond(a, b);
let r = crate::canvas::value_box_in(
ui,
(egui::Id::new(("sc_hole_ref_area", *k)), egui::Id::new(("sc_hole_ref", *k))),
(egui::Id::unique(("sc_hole_ref_area", *k)), egui::Id::unique(("sc_hole_ref", *k))),
label,
value,
at,
+2 -2
View File
@@ -227,7 +227,7 @@ fn without_value_boxes_a_number_typed_after_an_edge_pick_sets_its_distance() {
let left = screen(&mut h, Vec3::new(0.0, 20.0, 10.0));
h.step(&json!({"click": left})).unwrap();
h.frames(2);
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new(("sc_hole_ref_area", 0usize)))).is_none(), "a distance box in the view");
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::unique(("sc_hole_ref_area", 0usize)))).is_none(), "a distance box in the view");
h.step(&json!({"text": "7 mm"})).unwrap();
h.frames(2);
assert_eq!(distances(&h)[0], "7 mm");
@@ -367,7 +367,7 @@ fn a_pick_field_clicked_into_waits_for_its_pick_and_says_what_it_takes() {
/// nothing leaves the field waiting and the note shown. A new note scrolls into view once.
#[test]
fn a_pick_fields_note_goes_with_a_value_field_or_a_marquee_that_takes_something() {
let note_shown = egui::Id::new("sc_dialog_pick_refused_shown");
let note_shown = egui::Id::unique("sc_dialog_pick_refused_shown");
let mut h = plate();
h.step(&json!({"start": "solid.cylinder"})).unwrap();
h.frames(2);
+3 -3
View File
@@ -624,13 +624,13 @@ enum FileAction {
/// File view replaces the workspace while keeping the open design and its history intact.
pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
poll(app, ui.ctx());
let file_menu_open = ui.ctx().data(|data| data.get_temp::<bool>(egui::Id::new("sc_file_menu"))).unwrap_or(false);
let file_menu_open = ui.ctx().data(|data| data.get_temp::<bool>(egui::Id::unique("sc_file_menu"))).unwrap_or(false);
let interactive = !app.file_picker_busy() && !app.file_load.busy() && !file_menu_open;
// Help windows draw before Home; remember their prior visibility so one Escape closes only the window.
let had_help_window = ui.ctx().memory(|memory| {
["sc_about", "sc_crash_reports"]
.iter()
.any(|id| memory.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Middle, egui::Id::new(id))))
.any(|id| memory.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Middle, egui::Id::unique(id))))
});
if interactive
@@ -1503,7 +1503,7 @@ mod tests {
h.call("ui.click", serde_json::json!({"x": gear.x, "y": gear.y}));
assert!(h.app.prefs_window.open && h.app.home.open);
let shown = |h: &crate::scenario::Harness| {
h.ctx.memory(|m| m.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Middle, egui::Id::new("sc_prefs"))))
h.ctx.memory(|m| m.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Middle, egui::Id::unique("sc_prefs"))))
};
assert!(shown(&h), "the Preferences window must be drawn on Home");
h.call("ui.prefs", serde_json::json!({"button": "cancel"}));
+2 -2
View File
@@ -34,7 +34,7 @@ struct Cache(Arc<Mutex<CacheEntries>>);
/// Drop rasterised icon textures so the next paint uploads them again (wake from sleep, #8749).
pub(crate) fn clear_texture_cache(ctx: &egui::Context) {
let cache = ctx.data_mut(|d| d.get_temp_mut_or_insert_with(egui::Id::new("sc_icon_cache"), Cache::default).clone());
let cache = ctx.data_mut(|d| d.get_temp_mut_or_insert_with(egui::Id::unique("sc_icon_cache"), Cache::default).clone());
if let Ok(mut entries) = cache.0.lock() {
entries.textures.clear();
}
@@ -118,7 +118,7 @@ fn paint_resolved(p: &Painter, r: Rect, resolved: icon_svg::Resolved, look: Look
let ppp = ctx.pixels_per_point().max(0.5);
let side = r.width().min(r.height());
let px = (side * ppp).round().clamp(8.0, 512.0) as u32;
let cache = ctx.data_mut(|d| d.get_temp_mut_or_insert_with(egui::Id::new("sc_icon_cache"), Cache::default).clone());
let cache = ctx.data_mut(|d| d.get_temp_mut_or_insert_with(egui::Id::unique("sc_icon_cache"), Cache::default).clone());
let Ok(mut entries) = cache.0.lock() else { return };
let palette = crate::theme::Tokens::get().icon_palette;
if entries.palette != palette {
+1 -1
View File
@@ -108,7 +108,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, proj: &Proj) {
}
parts.push((egui::Rect::from_center_size(knob, Screen::splat(HANDLE * 3.0)), Part::Turn, egui::CursorIcon::Crosshair));
for (i, (rect, part, cursor)) in parts.into_iter().enumerate() {
let response = ui.interact(rect, egui::Id::new(("sc_insert_handle", i)), Sense::drag()).on_hover_cursor(cursor);
let response = ui.interact(rect, egui::Id::unique(("sc_insert_handle", i)), Sense::drag()).on_hover_cursor(cursor);
crate::scenario::publish_handle(&format!("insert:handle:{i}"), rect.center());
if response.drag_started()
&& let Some(from) = response.interact_pointer_pos().and_then(|p| pointer_on_sketch(&plane, proj, p))
+2 -2
View File
@@ -88,7 +88,7 @@ pub fn start(app: &mut SolveApp, offer: Offer) {
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let Some(steps) = app.install_steps else { return };
let (lines, after) = steps.messages();
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_install_steps"), "web", crate::i18n::tr("install.title"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_install_steps"), "web", crate::i18n::tr("install.title"))
.actions("", crate::i18n::tr("install.done"), None)
.show(ctx, true, |ui| {
ui_kit::text(ui, crate::i18n::tr("install.steps_intro"), ui_kit::Text::Report("intro", false));
@@ -121,7 +121,7 @@ mod tests {
/// Open the File menu and look for Install app….
fn file_menu_item(h: &mut Harness) -> bool {
h.ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_file_menu"), true));
h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique("sc_file_menu"), true));
h.frames(2);
h.widget(crate::i18n::tr(ITEM)).is_some()
}
+33 -41
View File
@@ -809,7 +809,7 @@ pub(crate) fn hold_input(app: &mut SolveApp, raw: &mut egui::RawInput) {
}
/// What [`hold_input`] keeps for the form.
fn typing(event: &egui::Event) -> bool {
pub(crate) fn typing(event: &egui::Event) -> bool {
matches!(
event,
egui::Event::Text(_) | egui::Event::Key { .. } | egui::Event::Paste(_) | egui::Event::Copy | egui::Event::Cut | egui::Event::Ime(_)
@@ -961,8 +961,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
crate::dim_view::drag_end();
crate::sketch3d::drag_end();
crate::drag_snap::clear();
crate::gizmo::cancel(ctx, egui::Id::new("sc_move_triad"));
crate::gizmo::cancel(ctx, egui::Id::new("sc_occ_triad"));
crate::gizmo::cancel(ctx, egui::Id::unique("sc_move_triad"));
crate::gizmo::cancel(ctx, egui::Id::unique("sc_occ_triad"));
if let Some(focus) = app.issue_report.cad_focus.take() {
ctx.memory_mut(|m| m.request_focus(focus));
}
@@ -1167,7 +1167,7 @@ mod tests {
h.app.home.open = false;
h.app.issue_report.draft = Some(draft_for(&h.app, None, None));
h.frames(16);
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("issue_report_body")));
h.frames(1);
h.frame_with(vec![egui::Event::Paste("故障".repeat(1500))]);
h.frames(4);
@@ -1202,7 +1202,7 @@ mod tests {
let commands = open_commands(&mut h);
assert!(!commands.iter().any(|c| matches!(c, egui::OutputCommand::OpenUrl(_))));
assert!(h.app.issue_report.draft.as_ref().unwrap().missing_description);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("issue_report_body")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("issue_report_body")));
h.step(&json!({"text":"The preview disappeared"})).unwrap();
let commands = open_commands(&mut h);
assert!(commands.iter().any(|c| matches!(c, egui::OutputCommand::OpenUrl(url) if url.url.starts_with(ISSUE_URL))));
@@ -1219,11 +1219,11 @@ mod tests {
h.app.home.open = false;
h.app.issue_report.draft = Some(draft_for(&h.app, None, None));
h.frames(16);
let description = Some(egui::Id::new("issue_report_body"));
let description = Some(egui::Id::unique("issue_report_body"));
open_commands(&mut h);
assert_eq!(h.ctx.memory(|m| m.focused()), description);
// Click the Summary field (found by its id: it has no label of its own) and type.
let title = egui::Id::new("issue_report_title").accesskit_id();
let title = egui::Id::unique("issue_report_title").accesskit_id();
let tree = h.output.accesskit_update.as_ref().unwrap();
let b = tree.nodes.iter().find(|(id, _)| *id == title).and_then(|(_, n)| n.bounds()).unwrap();
h.step(&json!({"click":[(b.x0 + b.x1) / 2.0, (b.y0 + b.y1) / 2.0]})).unwrap();
@@ -1257,28 +1257,20 @@ mod tests {
h.step(&json!({"widget":"Technical details"})).unwrap();
h.call("ui.scroll", json!({"x":800,"y":600,"delta":-1000}));
}
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new(field)));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique(field)));
h.frames(1);
let [x, y] = h.widget("Open GitHub issue form").unwrap();
let pos = egui::pos2(x as f32, y as f32);
let modifiers = egui::Modifiers { ctrl: true, command: true, ..Default::default() };
let raw = egui::RawInput {
screen_rect: Some(h.ctx.content_rect()),
focused: true,
events: vec![
egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers },
egui::Event::Paste("Latest reviewed content".into()),
egui::Event::PointerMoved(pos),
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() },
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() },
],
..Default::default()
};
let app = &mut h.app;
let mut output = h.ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
h.frame_with(vec![
egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers },
egui::Event::Paste("Latest reviewed content".into()),
egui::Event::PointerMoved(pos),
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() },
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() },
]);
// The click waits one frame for the typing before it.
h.frame();
let draft = h.app.issue_report.draft.as_ref().unwrap();
let edited = match field {
"issue_report_body" => &draft.description,
@@ -1291,13 +1283,12 @@ mod tests {
let body = draft.report_body();
let (url, copy_needed) = draft_url(&draft.report_title(), &body);
assert!(
output.platform_output.commands.iter().any(|c| matches!(c, egui::OutputCommand::OpenUrl(u) if u.url == url)),
"handoff must use this frame's edits: {field}, clipboard={clipboard}"
h.output.commands.iter().any(|c| matches!(c, egui::OutputCommand::OpenUrl(u) if u.url == url)),
"handoff must use the click frame's edits: {field}, clipboard={clipboard}"
);
if copy_needed {
assert!(output.platform_output.commands.iter().any(|c| matches!(c, egui::OutputCommand::CopyText(text) if text == &body)));
assert!(h.output.commands.iter().any(|c| matches!(c, egui::OutputCommand::CopyText(text) if text == &body)));
}
output.textures_delta.clear();
assert!(draft.opened_github && !draft.missing_description);
}
}
@@ -1666,7 +1657,8 @@ mod tests {
draft.image = Some(Arc::new(ColorImage::new([1600, 1000], vec![egui::Color32::GREEN; 1_600_000])));
h.app.issue_report.draft = Some(draft);
h.frames(20);
let rect = |h: &crate::scenario::Harness| egui::containers::AreaState::load(&h.ctx, egui::Id::new("sc_issue_report")).unwrap().rect();
let rect =
|h: &crate::scenario::Harness| egui::containers::AreaState::load(&h.ctx, egui::Id::unique("sc_issue_report")).unwrap().rect();
let settled = rect(&h);
h.frames(120);
let idle = rect(&h);
@@ -1695,7 +1687,7 @@ mod tests {
}
h.app.issue_report.draft = Some(draft_for(&h.app, None, None));
h.frames(10);
let rect = |h: &crate::scenario::Harness| egui::containers::AreaState::load(&h.ctx, egui::Id::new("sc_issue_report")).unwrap().rect();
let rect = |h: &crate::scenario::Harness| egui::containers::AreaState::load(&h.ctx, egui::Id::unique("sc_issue_report")).unwrap().rect();
let at = rect(&h);
assert!((at.center().x - h.ctx.content_rect().center().x).abs() < 1.0, "sketch={sketch}: it opens at the top middle: {at:?}");
// On the title, clear of the close button.
@@ -1748,7 +1740,7 @@ mod tests {
h.frames(4);
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.redo.len(), h.app.session.revision);
for field in ["issue_report_title", "issue_report_body"] {
let id = egui::Id::new(field);
let id = egui::Id::unique(field);
h.ctx.memory_mut(|m| m.request_focus(id));
h.frames(1);
h.step(&json!({"text":"edited-report-marker"})).unwrap();
@@ -1787,7 +1779,7 @@ mod tests {
receive(&mut h.app, token, Arc::new(ColorImage::new([2, 2], vec![egui::Color32::GREEN; 4])));
h.frames(3);
for field in ["issue_report_title", "issue_report_body"] {
let id = egui::Id::new(field);
let id = egui::Id::unique(field);
h.ctx.memory_mut(|m| m.request_focus(id));
h.frames(2);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(id), "placed Text must not steal report focus");
@@ -1816,8 +1808,8 @@ mod tests {
fn issue_report_boundary_preserves_opacity_widget_state_and_real_focus() {
let mut app = app();
let ctx = crate::fonts::test_context();
let focus = egui::Id::new("report-review-focus");
let state = egui::Id::new("cad-layout-state");
let focus = egui::Id::unique("report-review-focus");
let state = egui::Id::unique("cad-layout-state");
begin(&mut app, &ctx, None, None);
let raw = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))),
@@ -1831,16 +1823,16 @@ mod tests {
ui.disable();
assert_eq!(ui.opacity(), 1.0, "capture must retain normal drawing opacity");
assert!(!ui.is_enabled());
Container::new(&ctx, Surface::Popup).area(egui::Id::new("cad-boundary-proof")).fade_in(false).show(&ctx, |ui| {
Container::new(&ctx, Surface::Popup).area(egui::Id::unique("cad-boundary-proof")).fade_in(false).show(&ctx, |ui| {
assert!(!ui.is_enabled());
assert_eq!(ui.opacity(), 1.0);
ui.ctx().data_mut(|d| d.insert_temp(state, 42_u32));
ui.ctx().memory_mut(|m| m.request_focus(egui::Id::new("underlying-editor")));
ui.ctx().memory_mut(|m| m.request_focus(egui::Id::unique("underlying-editor")));
});
drop(barrier);
assert_eq!(ctx.memory(|m| m.focused()), Some(focus));
assert_eq!(ctx.data(|d| d.get_temp::<u32>(state)), Some(42));
assert!(Container::new(&ctx, Surface::Popup).area(egui::Id::new("report-boundary-proof")).is_enabled());
assert!(Container::new(&ctx, Surface::Popup).area(egui::Id::unique("report-boundary-proof")).is_enabled());
});
output.textures_delta.clear();
app.issue_report.draft = None;
@@ -1913,7 +1905,7 @@ mod tests {
let token = h.app.issue_report.pending.as_ref().unwrap().token;
receive(&mut h.app, token, Arc::new(ColorImage::new([2, 2], vec![egui::Color32::GREEN; 4])));
h.frames(3);
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("issue_report_body")));
h.step(&json!({"text":"Typing here describes the bug, not a CAD dimension."})).unwrap();
h.step(&json!({"key":"Enter"})).unwrap();
h.step(&json!({"key":"Escape"})).unwrap();
@@ -1922,7 +1914,7 @@ mod tests {
assert!(h.app.issue_report.draft.as_ref().unwrap().description.contains("Typing here describes"));
h.step(&json!({"widget":"Close draft"})).unwrap();
h.frames(2);
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::unique("sc_dim_edit")));
h.step(&json!({"key":"Enter"})).unwrap();
assert!(!crate::dim_view::editing());
assert_eq!(h.app.session.undo.len(), before.1 + 1);
@@ -1950,7 +1942,7 @@ mod tests {
let token = h.app.issue_report.pending.as_ref().unwrap().token;
receive(&mut h.app, token, Arc::new(ColorImage::new([2, 2], vec![egui::Color32::GREEN; 4])));
h.frames(3);
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("issue_report_body")));
h.step(&json!({"text":"report only"})).unwrap();
h.step(&json!({"key":"Enter"})).unwrap();
h.step(&json!({"key":"Escape"})).unwrap();
+11 -5
View File
@@ -15,7 +15,7 @@ const ALL_GUIDANCE: [&str; 4] = [
fn answer(ui: &mut Ui, label: &str, hint: &str, text: &mut String, id: &str, rows: usize) -> egui::Response {
ui_kit::text(ui, tr_label(label), ui_kit::Text::Heading(label));
ui_kit::text(ui, tr_label(hint), ui_kit::Text::Note(hint));
let response = ui_kit::text_area(ui, tr_label(label), Id::new(id), text, rows, MAX_ANSWER);
let response = ui_kit::text_area(ui, tr_label(label), Id::unique(id), text, rows, MAX_ANSWER);
crate::scenario::publish_handle(id, response.rect.center());
super::truncate_chars(text, MAX_ANSWER);
// What doesn't fit is not added: near the limit, say how much room is left (#7191).
@@ -124,7 +124,7 @@ pub(super) fn show(app: &mut SolveApp, draft: &mut Draft, ctx: &egui::Context) -
let compact = cards * t.form_card_width < actions_width + 2.0 * t.space_12 + 2.0 * t.space_6 + t.space_8;
let open_label = if compact { "Open GitHub" } else { "Open GitHub issue form" };
let close_label = if compact { "" } else { "Close draft" };
let (closed, mut action) = ui_kit::Window::new(Id::new("sc_issue_report"), "bug_report", tr_label("Report a bug"))
let (closed, mut action) = ui_kit::Window::new(Id::unique("sc_issue_report"), "bug_report", tr_label("Report a bug"))
.cards(cards)
// Opens at the top middle, and its title band drags it off what the report is about
// (#9916).
@@ -184,7 +184,7 @@ pub(super) fn show(app: &mut SolveApp, draft: &mut Draft, ctx: &egui::Context) -
ui_kit::text(ui, tr_label("Summary"), ui_kit::Text::Heading("Summary"));
let response = ui_kit::bounded_cell_editor(
ui,
Id::new("issue_report_title"),
Id::unique("issue_report_title"),
&mut draft.title,
"A short title for the problem (optional)",
false,
@@ -232,8 +232,14 @@ pub(super) fn show(app: &mut SolveApp, draft: &mut Draft, ctx: &egui::Context) -
ui_kit::Text::Note("private"),
);
ui_kit::section(ui, "issue-report-technical", "Technical details", false, |ui| {
let response =
ui_kit::text_area(ui, tr_label("Technical details"), Id::new("issue_report_technical"), &mut draft.body, 8, MAX_BODY);
let response = ui_kit::text_area(
ui,
tr_label("Technical details"),
Id::unique("issue_report_technical"),
&mut draft.body,
8,
MAX_BODY,
);
if response.changed() {
draft.opened_github = false;
}
+13 -12
View File
@@ -263,16 +263,17 @@ pub(crate) fn show_warning(app: &mut SolveApp, ctx: &egui::Context) {
app.menu.joint_move_warning = None;
return;
}
let (closed, action) = crate::ui_kit::Confirm::new(egui::Id::new("sc_joint_move_blocked"), "status_warning", app.tr("joint_move.blocked_title"))
.tone(crate::ui_kit::Tone::Warning)
.actions(app.tr("ui.heading_ok"), "", None)
.show(ctx, true, |ui| {
if !blocked.part.is_empty() {
crate::ui_kit::text(ui, &blocked.part, crate::ui_kit::Text::Report("part", false));
}
crate::ui_kit::text(ui, &reason, crate::ui_kit::Text::Report("reason", false));
crate::ui_kit::text(ui, app.tr(hint), crate::ui_kit::Text::Hint("remedy"));
});
let (closed, action) =
crate::ui_kit::Confirm::new(egui::Id::unique("sc_joint_move_blocked"), "status_warning", app.tr("joint_move.blocked_title"))
.tone(crate::ui_kit::Tone::Warning)
.actions(app.tr("ui.heading_ok"), "", None)
.show(ctx, true, |ui| {
if !blocked.part.is_empty() {
crate::ui_kit::text(ui, &blocked.part, crate::ui_kit::Text::Report("part", false));
}
crate::ui_kit::text(ui, &reason, crate::ui_kit::Text::Report("reason", false));
crate::ui_kit::text(ui, app.tr(hint), crate::ui_kit::Text::Hint("remedy"));
});
app.viewport.dialog_rect = crate::scenario::rect_of("dialog:window");
if closed || action != crate::ui_kit::Action::None {
app.menu.joint_move_warning = None;
@@ -644,7 +645,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painte
if let Some(p) = pointer {
let at = (p + vec2(20.0, 22.0)).clamp(proj.rect.min, proj.rect.max - vec2(260.0, 82.0).min(proj.rect.size()));
let host = Container::new(ui.ctx(), Surface::Canvas);
host.area(egui::Id::new("joint_move_readout")).interactable(false).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
host.area(egui::Id::unique("joint_move_readout")).interactable(false).fixed_pos(at).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
let frame = host.frame(ui.ctx(), tk.overlay, crate::frame::Side::Right).inner_margin(tk.space_8);
Container::show_backing(ui, frame, |ui| {
let value = if d.axis.angular {
@@ -678,7 +679,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painte
}
let host = Container::new(ui.ctx(), Surface::Canvas);
let mut done = false;
host.area(egui::Id::new("joint_move_help")).fixed_pos(proj.rect.left_bottom() + vec2(16.0, -148.0)).show(ui.ctx(), |ui| {
host.area(egui::Id::unique("joint_move_help")).fixed_pos(proj.rect.left_bottom() + vec2(16.0, -148.0)).show(ui.ctx(), |ui| {
let frame = host.frame(ui.ctx(), tk.overlay, crate::frame::Side::Right).inner_margin(tk.space_8);
Container::show_backing(ui, frame, |ui| {
ui.label(app.tr("joint_move.help"));
+5 -5
View File
@@ -565,7 +565,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
// A Preferences search lists the matching shortcuts too, and one recorded from there takes
// the key like one on this page (#10043). Only a search field that has the keyboard keeps
// its keys: going back to it to type stops the recording.
if ctx.memory(|m| m.has_focus(egui::Id::new("prefs_search"))) && app.keymap.capturing.is_some() {
if ctx.memory(|m| m.has_focus(egui::Id::unique("prefs_search"))) && app.keymap.capturing.is_some() {
stop_capture(app);
}
// Keep a pending chord until it has settled and all keys are released. Modifier-only
@@ -631,7 +631,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
.allocate_ui_with_layout(egui::vec2(width, t.control_height), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui_kit::search_field(
ui,
egui::Id::new("sc_keymap_filter"),
egui::Id::unique("sc_keymap_filter"),
&mut app.keymap.filter,
crate::i18n::tr_label("Filter commands or keys"),
)
@@ -911,8 +911,8 @@ fn shortcut_row(
let name =
egui::Rect::from_min_size(rect.min + egui::vec2(0.0, t.space_4), egui::vec2(if beside { name_width } else { width }, label_height));
ui.painter().galley(egui::pos2(name.left(), name.center().y - label_galley.size().y / 2.0), label_galley, t.form_ink);
let response = ui.interact(name, ui.id().with("command"), egui::Sense::hover()).backing_on_hover_text(command.id);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), format!("{label} ({})", command.id)));
let response = ui.interact(name, ui.scope_id().with("command"), egui::Sense::hover()).backing_on_hover_text(command.id);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), format!("{label} ({})", command.id)));
let top = if beside { name.top() } else { name.bottom() + t.space_4 };
let total = widths.iter().sum::<f32>() + widths.len().saturating_sub(1) as f32 * t.space_8;
let left = if beside { rect.right() - actions_width - t.space_8 - total.min(controls_width) } else { rect.left() };
@@ -1099,7 +1099,7 @@ mod tests {
assert!(!h.app.agent_chat.open, "extra modifiers do not match");
h.step(&json!({"key":"J", "cmd":true, "shift":true})).unwrap();
assert!(h.app.agent_chat.open);
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("sc_agent_chat_input")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("sc_agent_chat_input")));
h.frames(2);
h.step(&json!({"key":"J", "cmd":true, "shift":true})).unwrap();
assert!(!h.app.agent_chat.open, "a chord can hide Assistant from its input field");
+50 -16
View File
@@ -743,6 +743,8 @@ pub struct SolveApp {
pub synthetic: Vec<egui::Event>,
/// The modifiers of the synthetic press in progress (a Ctrl-drag keeps Ctrl while it moves).
pub synthetic_mods: egui::Modifiers,
/// A click that came in the same frame as typing before it, held for the next frame.
held_click: Vec<egui::Event>,
control_rx: Option<Receiver<ControlRequest>>,
/// Control requests that change the design, held while a file loads or an agent's design
/// change computes.
@@ -760,6 +762,9 @@ pub struct SolveApp {
pending_shots: Vec<(u64, Option<String>, std::sync::mpsc::Sender<Value>, f64)>,
shot_chat_turns: std::collections::HashMap<u64, std::sync::Arc<std::sync::atomic::AtomicBool>>,
queued_shots: Vec<(u64, f64, u32)>,
/// Screenshots egui hands back, by token; egui calls back, `collect_screenshots` reads them.
shot_tx: std::sync::mpsc::Sender<(u64, std::sync::Arc<egui::ColorImage>)>,
shot_rx: std::sync::mpsc::Receiver<(u64, std::sync::Arc<egui::ColorImage>)>,
shot_token: u64,
styled: bool,
fitted: bool,
@@ -815,6 +820,7 @@ impl SolveApp {
#[cfg(not(target_arch = "wasm32"))]
solidcraft_engine::slicer_files::clean_old();
let tree = browser::TreeState::for_session(session.uid);
let (shot_tx, shot_rx) = std::sync::mpsc::channel();
let mut app = SolveApp {
panel_runtime: None,
viewport_status: Default::default(),
@@ -926,6 +932,7 @@ impl SolveApp {
frame_rate: frame_rate::FrameRate::default(),
synthetic: Vec::new(),
synthetic_mods: egui::Modifiers::default(),
held_click: Vec::new(),
control_rx: None,
agent_chat: Default::default(),
ai_connect: Default::default(),
@@ -936,6 +943,8 @@ impl SolveApp {
pending_shots: Vec::new(),
shot_chat_turns: Default::default(),
queued_shots: Vec::new(),
shot_tx,
shot_rx,
shot_token: 0,
styled: false,
fitted: false,
@@ -2988,7 +2997,7 @@ impl SolveApp {
self.return_from_extrude_view();
agent_cursor::step(self, ctx);
browser::eye_drag_frame(self, ctx);
if !self.synthetic.is_empty() {
if !self.synthetic.is_empty() || !self.held_click.is_empty() {
ctx.request_repaint();
}
issue_report::tick(self, ctx);
@@ -3028,9 +3037,28 @@ impl SolveApp {
/// capture hides the report waits for it (the control channel's typing too).
pub fn raw_input_hook(&mut self, raw: &mut egui::RawInput) {
self.synthetic_input(raw);
finite_input(raw);
self.hold_click_after_typing(raw);
issue_report::hold_input(self, raw);
}
/// A pointer button after typing in the same frame (a slow frame) waits for the next frame,
/// with the events after it. egui 0.37 drops a field's focus on a click elsewhere before the
/// field reads the frame's typing, which lost what was typed. Drop once emilk/egui#8763 is fixed.
fn hold_click_after_typing(&mut self, raw: &mut egui::RawInput) {
if !self.held_click.is_empty() {
let mut events = std::mem::take(&mut self.held_click);
events.append(&mut raw.events);
raw.events = events;
}
let button = raw.events.iter().position(|e| matches!(e, egui::Event::PointerButton { .. }));
if let Some(i) = button
&& raw.events[..i].iter().any(issue_report::typing)
{
self.held_click = raw.events.split_off(i);
}
}
/// Inject synthetic events (one pointer event per frame).
fn synthetic_input(&mut self, raw: &mut egui::RawInput) {
if self.synthetic.is_empty() {
@@ -3420,10 +3448,15 @@ impl SolveApp {
// A view that just moved is drawn with fewer AO samples: shoot the full-quality frame
// that follows (#7165), or after half a second a view that keeps moving as it is.
let settled = viewport::ao_settled(self, ctx);
let shot_tx = &self.shot_tx;
self.queued_shots.retain_mut(|(token, at, frames)| {
*frames += 1;
if now >= *at && *frames >= 3 && (settled || now >= *at + 500.0) {
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new(*token)));
let (token, tx, ctx2) = (*token, shot_tx.clone(), ctx.clone());
ctx.request_screenshot(move |image| {
let _ = tx.send((token, image));
ctx2.request_repaint();
});
false
} else {
true
@@ -3448,23 +3481,13 @@ impl SolveApp {
true
}
});
// Drained every frame: a shot that comes after its request was cancelled or timed out
// is dropped here instead of waiting in the channel.
let shots: Vec<_> = self.shot_rx.try_iter().collect();
if self.pending_shots.is_empty() && !issue_report::pending(self) {
return;
}
let events: Vec<_> = ctx.input(|i| {
i.raw
.events
.iter()
.filter_map(|e| match e {
egui::Event::Screenshot { user_data, image, .. } => {
let token = user_data.data.as_ref().and_then(|d| d.downcast_ref::<u64>()).copied()?;
Some((token, image.clone()))
}
_ => None,
})
.collect()
});
for (token, image) in events {
for (token, image) in shots {
issue_report::receive(self, token, image.clone());
if let Some(i) = self.pending_shots.iter().position(|(t, ..)| *t == token) {
let (_, path, reply, _) = self.pending_shots.remove(i);
@@ -3486,6 +3509,17 @@ impl SolveApp {
}
}
/// Drop pointer, wheel and gesture events that carry a non-finite number: egui 0.37 asserts on them.
fn finite_input(raw: &mut egui::RawInput) {
raw.events.retain(|event| match event {
egui::Event::PointerMoved(pos) | egui::Event::PointerButton { pos, .. } | egui::Event::MiddleClickPaste { pos, .. } => pos.is_finite(),
egui::Event::MouseMoved(delta) | egui::Event::MouseWheel { delta, .. } => delta.is_finite(),
egui::Event::Zoom(amount) | egui::Event::Rotate(amount) => amount.is_finite(),
egui::Event::Touch { pos, force, .. } => pos.is_finite() && force.is_none_or(f32::is_finite),
_ => true,
});
}
/// The window is minimised or fully covered, so it presents no frames (#1677). Unknown counts as
/// shown: the screenshot deadline still ends a wait for a frame that never comes.
fn window_hidden(ctx: &egui::Context) -> bool {
+1 -1
View File
@@ -251,7 +251,7 @@ pub fn link_copies(app: &mut SolveApp, accept: bool) -> bool {
pub fn show_copies(app: &mut SolveApp, ctx: &egui::Context) {
let Some(c) = app.menu.link_copies.clone() else { return };
let name = app.session.doc.components.iter().find(|x| x.id == c.linked).map(|x| x.name.clone()).unwrap_or_default();
let (closed, action) = crate::ui_kit::Confirm::new(egui::Id::new("sc_link_copies"), "help", crate::i18n::tr_label("Link Identical Copies"))
let (closed, action) = crate::ui_kit::Confirm::new(egui::Id::unique("sc_link_copies"), "help", crate::i18n::tr_label("Link Identical Copies"))
.actions("Link", "Keep", None)
.show(ctx, true, |ui| {
let text = crate::i18n::format("ui.link_copies_text", &[("name", &name)]);
+1 -1
View File
@@ -379,7 +379,7 @@ fn appearance_card_keeps_the_approved_width_with_no_preselection() {
h.app.start("appearance.assign");
h.frames(4);
let header = scenario::rect_of("dialog:header").unwrap();
let card = h.ctx.memory(|m| m.area_rect(egui::Id::new(("sc_dialog", "APPEARANCE")))).unwrap();
let card = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("sc_dialog", "APPEARANCE")))).unwrap();
let t = Tokens::get();
let approved = t.form_card_width + t.form_label_width + t.space_6;
assert!((card.width() - approved).abs() <= t.space_2, "card {card:?}, header {header:?}");
+10 -4
View File
@@ -65,7 +65,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let anchor = crate::frame::offset_anchor(docked, moved, drag_bounds, side);
let panel = ui_kit::Panel::new(
title,
egui::Id::new("sc_material_card_review"),
egui::Id::unique("sc_material_card_review"),
anchor,
drag_bounds,
side,
@@ -95,8 +95,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|ui| {
ui_kit::text(ui, tr("ui.material_review.scope"), ui_kit::Text::Hint("manage"));
ui_kit::text(ui, tr("ui.material_review.input"), ui_kit::Text::Report("material_input_label", false));
let response =
ui_kit::text_area(ui, tr("ui.material_review.input"), egui::Id::new("sc_material_card_input"), &mut window.input, 5, MAX_BYTES);
let response = ui_kit::text_area(
ui,
tr("ui.material_review.input"),
egui::Id::unique("sc_material_card_input"),
&mut window.input,
5,
MAX_BYTES,
);
crate::scenario::publish_handle("review:input", response.rect.center());
if response.changed() {
window.reviewed = None;
@@ -272,7 +278,7 @@ mod tests {
assert!(h.widget("Original synthetic candidate").is_some());
assert!(h.widget("Density: Unknown").is_some());
assert!(h.widget("E: 1.2e10 Pa").is_some());
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("sc_material_card_input")));
h.ctx.memory_mut(|m| m.request_focus(egui::Id::unique("sc_material_card_input")));
h.step(&json!({"text":" "})).unwrap();
assert!(h.app.material_card_review.as_ref().unwrap().reviewed.is_none(), "editing invalidates the old report");
h.step(&json!({"click":{"handle":"dialog:close"}})).unwrap();
+3 -2
View File
@@ -301,8 +301,9 @@ fn number(value: &str, field: &str) -> Result<f64, String> {
fn field(ui: &mut egui::Ui, label: &str, id: &str, value: &mut String, unit: &str) {
let label = if unit.is_empty() { label.to_string() } else { format!("{label} ({unit})") };
let response =
ui_kit::row(ui, &label, |ui| ui_kit::cell_editor(ui, egui::Id::new(("physical_editor", id)), value, tr("ui.material_review.unknown"), false));
let response = ui_kit::row(ui, &label, |ui| {
ui_kit::cell_editor(ui, egui::Id::unique(("physical_editor", id)), value, tr("ui.material_review.unknown"), false)
});
crate::scenario::publish_handle(&format!("material:field:{id}"), response.rect.center());
}
+1 -1
View File
@@ -1486,7 +1486,7 @@ fn menus_near_the_window_corner_open_inward() {
assert_eq!(h.call("ui.menu", json!({}))["result"]["open"], true, "{scale} {target:?}");
// An auto-sized menu takes a few frames to settle its size.
h.frames(3);
let r = h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_context_menu"))).unwrap();
let r = h.ctx.memory(|m| m.area_rect(egui::Id::unique("sc_context_menu"))).unwrap();
assert!(screen.contains_rect(r), "{scale} {target:?}: {r:?} outside {screen:?}");
// Below and right of the cursor where it fits, else ending at it.
let x = if at.x + r.width() <= screen.right() { at.x } else { at.x - r.width() };
+6 -3
View File
@@ -109,8 +109,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
let t = Tokens::get();
let now = crate::now_ms();
let (closed, _) =
crate::ui_kit::Window::new(egui::Id::new("sc_messages"), "list", tr("messages.title")).fields().actions("", "", None).show(ctx, true, |ui| {
let (closed, _) = crate::ui_kit::Window::new(egui::Id::unique("sc_messages"), "list", tr("messages.title")).fields().actions("", "", None).show(
ctx,
true,
|ui| {
ui.horizontal(|ui| {
let copy = crate::ui_kit::button(ui, tr("messages.copy_all"), crate::ui_kit::Button::Enabled(app.messages.iter().len() > 0));
crate::scenario::publish_handle("messages:copy", copy.rect.center());
@@ -137,7 +139,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
});
}
});
});
},
);
if closed {
app.messages.open = false;
}
+9 -6
View File
@@ -521,10 +521,13 @@ fn paint_open(app: &SolveApp, ctx: &egui::Context) {
let t = Tokens::get();
let pointer = ctx.input(|i| i.pointer.latest_pos());
let area = Rect::from_center_size(wheel.centre, vec2(2.0 * (RING + 6.0), 2.0 * (RING + 6.0)));
Container::new(ctx, Surface::Canvas).area(egui::Id::new("sc_mouse_gesture")).fixed_pos(area.min).order(egui::Order::Foreground).show(ctx, |ui| {
let (r, _) = ui.allocate_exact_size(area.size(), Sense::hover());
paint(&ui.painter_at(r.expand(4.0)), wheel.centre, 1.0, wheel.studio, &list, pointer, t);
});
Container::new(ctx, Surface::Canvas).area(egui::Id::unique("sc_mouse_gesture")).fixed_pos(area.min).order(egui::Order::Foreground).show(
ctx,
|ui| {
let (r, _) = ui.allocate_exact_size(area.size(), Sense::hover());
paint(&ui.painter_at(r.expand(4.0)), wheel.centre, 1.0, wheel.studio, &list, pointer, t);
},
);
}
// ---------------------------------------------------------------------------------------------
@@ -632,7 +635,7 @@ fn editor(app: &mut SolveApp, ctx: &egui::Context) {
let t = Tokens::get();
let mut studio = app.gesture.editor.studio;
let mut config = app.preferences.mouse_gestures.clone();
let closed = crate::ui_kit::Window::new(egui::Id::new("sc_gesture_editor"), "settings", crate::i18n::tr_label("Mouse gestures"))
let closed = crate::ui_kit::Window::new(egui::Id::unique("sc_gesture_editor"), "settings", crate::i18n::tr_label("Mouse gestures"))
.cards(3.0)
.show_body(ctx, |ui| {
crate::ui_kit::checkbox(ui, crate::i18n::tr_label("Ring of tools on the right button"), &mut config.enabled);
@@ -688,7 +691,7 @@ fn editor(app: &mut SolveApp, ctx: &egui::Context) {
&shown,
egui::PopupCloseBehavior::CloseOnClickOutside,
|ui| {
let filter_id = ui.id().with("filter");
let filter_id = ui.scope_id().with("filter");
let mut filter: String = ui.data_mut(|d| d.get_temp(filter_id).unwrap_or_default());
crate::ui_kit::text_field(ui, &mut filter, crate::i18n::tr_label("Filter…"));
ui.data_mut(|d| d.insert_temp(filter_id, filter.clone()));
+65 -33
View File
@@ -55,7 +55,13 @@ impl WebWheelGestures {
use egui::{Event, Modifiers, MouseWheelUnit, TouchPhase, Vec2};
const IDLE: f64 = 0.1;
const WHEEL_STEP_MIN: f32 = 40.0;
let phase_event = |phase| Event::MouseWheel { unit: MouseWheelUnit::Point, delta: Vec2::ZERO, modifiers: Modifiers::NONE, phase };
let phase_event = |phase| Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: MouseWheelUnit::Point,
delta: Vec2::ZERO,
modifiers: Modifiers::NONE,
phase,
};
let valid_time = now.is_finite() && now >= 0.0;
let events = std::mem::take(&mut raw.events);
if self.last_scroll.is_some_and(|last| !valid_time || now < last || now - last >= IDLE) {
@@ -190,7 +196,7 @@ pub(crate) fn immediate_wheel(
mouse_pan: egui::Vec2::ZERO,
};
for event in &input.events {
let egui::Event::MouseWheel { unit, delta, modifiers, phase } = event else { continue };
let egui::Event::MouseWheel { unit, delta, modifiers, phase, .. } = event else { continue };
let modifier_zoom = claims_scroll(*modifiers, zoom_modifier);
if !modifier_zoom && modifiers.matches_any(options.zoom_modifier) {
continue;
@@ -218,11 +224,7 @@ pub(crate) fn immediate_wheel(
}
egui::TouchPhase::Move => {}
}
let points = match unit {
egui::MouseWheelUnit::Point => *delta,
egui::MouseWheelUnit::Line => *delta * options.line_scroll_speed,
egui::MouseWheelUnit::Page => *delta * page_height,
};
let points = wheel_points(*unit, *delta, options, page_height);
if !points.is_finite() {
continue;
}
@@ -256,6 +258,16 @@ pub(crate) fn immediate_wheel(
out
}
/// A wheel event's movement in points, in egui's units: lines at its line scroll speed, pages at
/// `page_height`.
fn wheel_points(unit: egui::MouseWheelUnit, delta: egui::Vec2, options: egui::InputOptions, page_height: f32) -> egui::Vec2 {
match unit {
egui::MouseWheelUnit::Point => delta,
egui::MouseWheelUnit::Line => delta * options.line_scroll_speed,
egui::MouseWheelUnit::Page => delta * page_height,
}
}
/// Whether the chosen trackpad zoom modifier (Preferences › Navigation, #8322) claims a wheel
/// event: that key held alone. egui aliases Ctrl/Command on Windows/Linux; macOS can hold both.
pub(crate) fn claims_scroll(modifiers: egui::Modifiers, zoom_modifier: Option<egui::Modifiers>) -> bool {
@@ -277,14 +289,10 @@ pub(crate) fn pinch_factor(input: &egui::InputState, options: egui::InputOptions
}
}
// A wheel the chosen zoom modifier already turns into zoom is not also a pinch.
egui::Event::MouseWheel { unit, delta, modifiers, phase: egui::TouchPhase::Move }
egui::Event::MouseWheel { unit, delta, modifiers, phase: egui::TouchPhase::Move, .. }
if modifiers.matches_any(options.zoom_modifier) && !claims_scroll(*modifiers, zoom_modifier) =>
{
let points = match unit {
egui::MouseWheelUnit::Point => *delta,
egui::MouseWheelUnit::Line => *delta * options.line_scroll_speed,
egui::MouseWheelUnit::Page => *delta * page_height,
};
let points = wheel_points(*unit, *delta, options, page_height);
let f = (options.scroll_zoom_speed * (points.x + points.y)).exp();
if f.is_finite() && f > 0.0 {
factor *= f;
@@ -305,15 +313,11 @@ pub(crate) fn axis_wheel(ctx: &egui::Context, page_height: f32) -> (f32, f32, f3
let (zoom, sideways, pinch) = ctx.input(|i| {
let (mut zoom, mut sideways) = (0.0, 0.0);
for event in &i.events {
let egui::Event::MouseWheel { unit, delta, modifiers, phase } = event else { continue };
let egui::Event::MouseWheel { unit, delta, modifiers, phase, .. } = event else { continue };
if *phase != egui::TouchPhase::Move || modifiers.matches_any(options.zoom_modifier) {
continue;
}
let points = match unit {
egui::MouseWheelUnit::Point => *delta,
egui::MouseWheelUnit::Line => *delta * options.line_scroll_speed,
egui::MouseWheelUnit::Page => *delta * page_height,
};
let points = wheel_points(*unit, *delta, options, page_height);
let up = route_wheel_vertical(points, *modifiers, options);
zoom += up;
sideways += if up == 0.0 { points.x + points.y } else { points.x };
@@ -425,7 +429,7 @@ fn view_keys_blocked(app: &SolveApp, ctx: &egui::Context) -> bool {
|| app.viewport.context_menu.is_some()
|| app.ui.palette_open
|| ctx.memory(|m| m.focused().is_some())
|| ctx.data(|d| d.get_temp::<egui::Id>(egui::Id::new("sc_panel_open")).is_some())
|| ctx.data(|d| d.get_temp::<egui::Id>(egui::Id::unique("sc_panel_open")).is_some())
}
/// View keys (#1541): the arrows turn the view 15° (with Shift a quarter turn, as the navigation
@@ -1250,7 +1254,7 @@ fn orbit_camera(app: &mut SolveApp, dx: f64, dy: f64) {
mod tests {
use solidcraft_engine::geom::prelude::*;
fn web_wheel(unit: egui::MouseWheelUnit, delta: egui::Vec2, modifiers: egui::Modifiers) -> egui::Event {
egui::Event::MouseWheel { unit, delta, modifiers, phase: egui::TouchPhase::Move }
egui::Event::MouseWheel { source: egui::MouseWheelSource::Unknown, unit, delta, modifiers, phase: egui::TouchPhase::Move }
}
fn web_frame(
@@ -1297,7 +1301,13 @@ mod tests {
] {
for unit in [U::Point, U::Line, U::Page] {
for bracket in [false, true] {
let event = |phase| egui::Event::MouseWheel { unit, delta: vec2(8.0, 12.0), modifiers: held, phase };
let event = |phase| egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit,
delta: vec2(8.0, 12.0),
modifiers: held,
phase,
};
let mut input = egui::InputState::default();
input.events = if bracket { vec![event(P::Start), event(P::Move), event(P::End)] } else { vec![event(P::Move)] };
let raw = super::immediate_wheel(&input, options, 600.0, &mut false, &mut Default::default(), true, chosen);
@@ -1452,7 +1462,7 @@ mod tests {
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
app.viewport.rect = Some(rect);
let response = ui.interact(rect, ui.id().with("touchpad-setting"), egui::Sense::click_and_drag());
let response = ui.interact(rect, ui.scope_id().with("touchpad-setting"), egui::Sense::click_and_drag());
super::handle(app, ui, rect, &response, caps);
});
out.textures_delta.clear();
@@ -1973,7 +1983,7 @@ mod tests {
};
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
let resp = ui.interact(rect, ui.id().with("drag-zoom"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("drag-zoom"), egui::Sense::click_and_drag());
super::handle(&mut app, ui, rect, &resp, caps);
});
out.textures_delta.clear();
@@ -2026,13 +2036,14 @@ mod tests {
};
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
let resp = ui.interact(rect, ui.id().with("wheel-speed"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("wheel-speed"), egui::Sense::click_and_drag());
super::handle(&mut app, ui, rect, &resp, caps);
});
out.textures_delta.clear();
};
frame(vec![Event::PointerMoved(pos2(200.0, 300.0))]);
frame(vec![Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: MouseWheelUnit::Point,
delta: vec2(0.0, 40.0),
modifiers: Modifiers::NONE,
@@ -2109,7 +2120,7 @@ mod tests {
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
app.viewport.rect = Some(rect);
let resp = ui.interact(rect, ui.id().with("spin"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("spin"), egui::Sense::click_and_drag());
super::handle(app, ui, rect, &resp, super::NavCaps::MODEL);
});
out.textures_delta.clear();
@@ -2179,7 +2190,7 @@ mod tests {
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
app.viewport.rect = Some(rect);
let resp = ui.interact(rect, ui.id().with("flat"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("flat"), egui::Sense::click_and_drag());
super::handle(app, ui, rect, &resp, flat);
});
out.textures_delta.clear();
@@ -2210,7 +2221,13 @@ mod tests {
frame(
&mut app,
modifiers,
vec![Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta: vec2(20.0, -10.0), modifiers, phase }],
vec![Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Point,
delta: vec2(20.0, -10.0),
modifiers,
phase,
}],
);
}
assert_eq!(app.cam.back(), before.back(), "{preset}: trackpad turned a flat page");
@@ -2220,6 +2237,7 @@ mod tests {
}
let before = app.cam;
let wheel = Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Line,
delta: vec2(0.0, 3.0),
modifiers: Modifiers::NONE,
@@ -2255,7 +2273,7 @@ mod tests {
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
app.viewport.rect = Some(rect);
let resp = ui.interact(rect, ui.id().with("native-pinch"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("native-pinch"), egui::Sense::click_and_drag());
super::handle(app, ui, rect, &resp, super::NavCaps::MODEL);
});
out.textures_delta.clear();
@@ -2267,7 +2285,13 @@ mod tests {
vec![
Event::ModifiersChanged(Modifiers::CTRL),
Event::PointerMoved(at),
Event::MouseWheel { unit: MouseWheelUnit::Point, delta: vec2(0.0, 50.0), modifiers: Modifiers::CTRL, phase: TouchPhase::Move },
Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: MouseWheelUnit::Point,
delta: vec2(0.0, 50.0),
modifiers: Modifiers::CTRL,
phase: TouchPhase::Move,
},
],
);
assert!(app.cam.distance < before.distance, "a positive Ctrl+wheel pinch zooms in: {} -> {}", before.distance, app.cam.distance);
@@ -2281,7 +2305,13 @@ mod tests {
&mut plain,
vec![
Event::PointerMoved(at),
Event::MouseWheel { unit: MouseWheelUnit::Point, delta: vec2(0.0, 50.0), modifiers: Modifiers::NONE, phase: TouchPhase::Move },
Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: MouseWheelUnit::Point,
delta: vec2(0.0, 50.0),
modifiers: Modifiers::NONE,
phase: TouchPhase::Move,
},
],
);
let pinched = before.distance / app.cam.distance;
@@ -2337,6 +2367,7 @@ mod tests {
for phase in [egui::TouchPhase::Start, egui::TouchPhase::End, egui::TouchPhase::Cancel] {
let mut input = egui::InputState::default();
input.events = vec![egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, 40.0),
modifiers: egui::Modifiers::CTRL,
@@ -2357,7 +2388,8 @@ mod tests {
input.events = events;
super::pinch_factor(&input, options, 600.0, None)
};
let wheel = |unit, delta, modifiers| Event::MouseWheel { unit, delta, modifiers, phase: TouchPhase::Move };
let wheel =
|unit, delta, modifiers| Event::MouseWheel { source: egui::MouseWheelSource::Unknown, unit, delta, modifiers, phase: TouchPhase::Move };
let close = |a: f32, b: f32| (a - b).abs() < 1e-4;
let point = factor(vec![wheel(MouseWheelUnit::Point, vec2(0.0, 40.0), Modifiers::CTRL)]);
assert!(close(point, (options.scroll_zoom_speed * 40.0).exp()), "point pinch {point}");
@@ -2394,7 +2426,7 @@ mod tests {
let mut out = ctx.run_ui(raw, |ui| {
let rect = ui.max_rect();
app.viewport.rect = Some(rect);
let resp = ui.interact(rect, ui.id().with("page"), egui::Sense::click_and_drag());
let resp = ui.interact(rect, ui.scope_id().with("page"), egui::Sense::click_and_drag());
super::handle(&mut app, ui, rect, &resp, caps);
});
out.textures_delta.clear();
+5 -6
View File
@@ -211,7 +211,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
return;
}
crate::scenario::publish_rect("nav:outer", bar.rect);
let menu_id = ui.id().with("navigation_bar_menu");
let menu_id = ui.scope_id().with("navigation_bar_menu");
let mut open = ui.data(|d| d.get_temp::<String>(menu_id)).unwrap_or_default();
let active = ui.rect_contains_pointer(bar.rect)
|| ui.rect_contains_pointer(crate::viewport::axis_rect(app, rect))
@@ -223,7 +223,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|| app.viewport.spin
|| !open.is_empty();
let opacity = ui_kit::navigation_opacity(app.preferences.navigation_bar.idle, active);
let background = ui.interact(bar.rect, ui.id().with("navigation_bar_context"), egui::Sense::click());
let background = ui.interact(bar.rect, ui.scope_id().with("navigation_bar_context"), egui::Sense::click());
let opacity = if app.preferences.navigation_bar.idle == Idle::Hide {
ui.ctx().animate_value_with_time(menu_id.with("opacity"), opacity, if active { ui_kit::NAV_FADE_SECONDS } else { 0.0 })
} else {
@@ -355,7 +355,7 @@ pub(crate) fn paint_row(
ui_kit::navigation_separator(ui, *bounds);
ui.interact(
*bounds,
ui.id().with("separator"),
ui.scope_id().with("separator"),
if editing.is_some() { egui::Sense::click_and_drag() } else { egui::Sense::click() },
)
} else {
@@ -386,15 +386,14 @@ pub(crate) fn paint_row(
.inner;
let mut hidden = None;
if let Some((selected, caps, render)) = editing {
response
.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected == Some(index), app.tr(slot.label)));
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), selected == Some(index), app.tr(slot.label)));
ui_kit::navigation_edit_outline(ui, *bounds, !slot.available(caps), selected == Some(index));
if !slot.available(caps) {
hidden = Some(if render { "navbar.custom.hidden_render" } else { "navbar.custom.hidden_design" });
}
} else {
response.widget_info(|| {
egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected(app, slot.id) || open == slot.id, app.tr(slot.label))
egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), selected(app, slot.id) || open == slot.id, app.tr(slot.label))
});
}
// The settings' preview keeps its tips above, wherever the bar sits in the view.
+2 -2
View File
@@ -173,7 +173,7 @@ pub(super) fn navigate(app: &mut SolveApp, action: Option<&str>, dir: Option<Vec
fn control(app: &mut SolveApp, ui: &mut egui::Ui, bounds: Rect, action: &str, tooltip: &str) -> bool {
crate::scenario::publish_handle(&format!("navigation_cube:control:{action}"), bounds.center());
let response = ui.interact(bounds, ui.id().with(("navigation_cube", action)), Sense::click()).backing_on_hover_text(tooltip);
let response = ui.interact(bounds, ui.scope_id().with(("navigation_cube", action)), Sense::click()).backing_on_hover_text(tooltip);
let hover = response.hovered();
if response.clicked() {
navigate(app, Some(action), None);
@@ -259,7 +259,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
crate::scenario::publish_count("axis_view_highlights", f64::from(u8::from(hovered.is_some())));
if let Some(face) = hovered.and_then(|i| faces.get(i)) {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
ui.interact(Rect::from_center_size(hover.unwrap_or(face.centre), vec2(2.0, 2.0)), ui.id().with("navigation_cube_tip"), Sense::hover())
ui.interact(Rect::from_center_size(hover.unwrap_or(face.centre), vec2(2.0, 2.0)), ui.scope_id().with("navigation_cube_tip"), Sense::hover())
.backing_on_hover_text(format!(
"{} view. Drag to orbit{}.",
face.label,
+1 -1
View File
@@ -118,7 +118,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let _language = crate::i18n::scope(&app.preferences.language);
let hosts = prompt.hosts.iter().map(|h| format!("\\\\{h}")).collect::<Vec<_>>().join(", ");
let detail = crate::i18n::format("network.prompt_detail", &[("host", &hosts)]);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_network_prompt"), "status_warning", crate::i18n::tr("network.prompt_title"))
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_network_prompt"), "status_warning", crate::i18n::tr("network.prompt_title"))
.tone(ui_kit::Tone::Warning)
.actions(crate::i18n::tr("network.allow_once"), crate::i18n::tr("ui.cancel"), Some(crate::i18n::tr("network.always_allow")))
.selected(ui_kit::Action::Cancel)
+1 -1
View File
@@ -270,7 +270,7 @@ pub fn update(app: &mut SolveApp, d: &mut Dialog, ctx: &egui::Context) {
/// Where a pick in an Operation row is noted for the next [`update`].
pub(crate) fn picked_id() -> egui::Id {
egui::Id::new("op_auto_picked")
egui::Id::unique("op_auto_picked")
}
/// The Operation row of a window that chooses by itself: any pick there, the shown option
+2 -2
View File
@@ -149,7 +149,7 @@ fn show_lock_prompt(app: &mut SolveApp, ctx: &egui::Context) {
"document.open_elsewhere_detail",
&[("name", &name), ("who", &who), ("when", &crate::recovery_ui::ago(lock.opened)), ("app", &lock.app)],
);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_open_elsewhere"), "status_warning", &title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_open_elsewhere"), "status_warning", &title)
.tone(ui_kit::Tone::Warning)
.actions(crate::i18n::tr("document.open_read_only"), crate::i18n::tr("ui.cancel"), Some(crate::i18n::tr("document.open_anyway")))
.selected(ui_kit::Action::Ok)
@@ -219,7 +219,7 @@ fn show_disk_prompt(app: &mut SolveApp, ctx: &egui::Context) {
let name = app.session.path.as_deref().map(file_name).unwrap_or_default();
let title = crate::i18n::format("document.changed_on_disk", &[("name", &name)]);
let detail = crate::i18n::format("document.changed_on_disk_detail", &[("name", &name)]);
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_changed_on_disk"), "status_warning", &title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_changed_on_disk"), "status_warning", &title)
.tone(ui_kit::Tone::Warning)
.actions(crate::i18n::tr("document.save_copy"), crate::i18n::tr("ui.cancel"), Some(crate::i18n::tr("document.overwrite")))
.selected(ui_kit::Action::Ok)
+1 -1
View File
@@ -165,7 +165,7 @@ pub(crate) fn show(app: &SolveApp, ctx: &egui::Context) {
let t = crate::theme::Tokens::get();
let at = egui::pos2(view.left() + 12.0, view.bottom() - 12.0);
Container::new(ctx, Surface::Toast)
.area(egui::Id::new("open_meshing"))
.area(egui::Id::unique("open_meshing"))
.order(egui::Order::Foreground)
.pivot(egui::Align2::LEFT_BOTTOM)
.fixed_pos(at)
+1 -1
View File
@@ -270,7 +270,7 @@ impl PanelHost for EguiPanelHost<'_> {
if let Some(status) = &presentation.status {
response = response.backing_on_hover_text(status);
}
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, true, &presentation.title));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, true, &presentation.title));
}
if slot == ContentSlot::Footer && !presentation.actions.is_empty() {
egui::ScrollArea::horizontal().id_salt("panel_actions").show(ui, |ui| {
+3 -3
View File
@@ -226,7 +226,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let title = crate::i18n::tr_label("Parameters");
let panel = ui_kit::Panel::new(
title,
egui::Id::new("sc_params_dialog"),
egui::Id::unique("sc_params_dialog"),
anchor,
drag_bounds,
side,
@@ -363,7 +363,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
/// Where the chosen order is kept: egui's persisted memory, like a section's open state.
fn sort_id() -> egui::Id {
egui::Id::new("sc_params_sort")
egui::Id::unique("sc_params_sort")
}
/// The dialog's sections, each in the chosen order.
@@ -862,7 +862,7 @@ struct Cache {
}
fn cache_id() -> egui::Id {
egui::Id::new("sc_param_completions")
egui::Id::unique("sc_param_completions")
}
/// Keep the completion list (parameters, functions, constants, units) for this revision of the
+3 -3
View File
@@ -308,7 +308,7 @@ struct PanelState {
}
fn panel_id() -> egui::Id {
egui::Id::new("personal_sheet_rules_panel")
egui::Id::unique("personal_sheet_rules_panel")
}
fn rule_manager(d: &mut Dialog) -> Option<&mut Sm> {
@@ -357,7 +357,7 @@ pub(crate) fn rows(ui: &mut egui::Ui, d: &mut Dialog, library: &mut Library, doc
}
});
response.widget_info(|| {
let mut info = egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, ui.is_enabled(), tr("sheet.personal_rules.rule"));
let mut info = egui::WidgetInfo::labeled(egui::Role::ComboBox, ui.is_enabled(), tr("sheet.personal_rules.rule"));
info.current_text_value = Some(display.clone());
info
});
@@ -442,7 +442,7 @@ pub(crate) fn name_prompt(ctx: &egui::Context, d: &mut Dialog, library: &mut Lib
let mut state = ctx.data_mut(|data| data.get_temp::<PanelState>(id)).unwrap_or_default();
let Some(mut prompt) = state.prompt.take() else { return false };
let title = tr(if prompt.renaming { "sheet.personal_rules.rename" } else { "sheet.personal_rules.save" });
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("personal_sheet_rule_name"), "save", title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("personal_sheet_rule_name"), "save", title)
.actions(if prompt.renaming { tr("sheet.personal_rules.rename") } else { crate::i18n::tr_label("Save") }, "Cancel", None)
.show(ctx, true, |ui| {
ui_kit::row(ui, "sheet.personal_rules.name", |ui| ui_kit::focused_text_field(ui, &mut prompt.name, tr("sheet.personal_rules.name_hint")));
+3 -3
View File
@@ -244,7 +244,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &mut egui::Ui, painter: &egui::Painte
let Some(bs) = proj.to_screen(base) else { return };
let t = Tokens::get();
let area = |c: Pos2| egui::Rect::from_center_size(c, vec2(2.0 * REACH, 2.0 * REACH));
let id = |what: &str| egui::Id::new(("sc_place", sp.name, what));
let id = |what: &str| egui::Id::unique(("sc_place", sp.name, what));
// The crosshair that moves the feature. The press lands where the finger is, not always the
// centre: the drag keeps that gap.
@@ -384,7 +384,7 @@ fn ring_radius(proj: &Proj, base: Vec3, x: Vec3, y: Vec3) -> Option<f64> {
/// `anchor` (the arrow's tip) or above it, whichever is clear first. Seen straight down the arrow
/// collapses onto the place and the box sits just right of it, where the ring's grip often is.
pub(crate) fn yield_box(ui: &egui::Ui, anchor: Pos2, at: Pos2) -> Pos2 {
let Some(size) = ui.ctx().memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).map(|r| r.size()) else { return at };
let Some(size) = ui.ctx().memory(|m| m.area_rect(egui::Id::unique("sc_canvas_area"))).map(|r| r.size()) else { return at };
let target = match RING_ABOUT.with(|c| c.get()) {
// With a ring, the box keeps out of the whole circle the ring, its grip, the compass and
// the readout sweep, whatever the turn: one place per view, nothing moves when a turn
@@ -403,7 +403,7 @@ pub(crate) fn yield_box(ui: &egui::Ui, anchor: Pos2, at: Pos2) -> Pos2 {
};
// The step aside slides, as an offset from where the box would be, so the box still follows
// the arrow's tip at once when the view moves.
at + motion::slide(ui.ctx(), egui::Id::new("sc_place_box"), target - at, DIM_S)
at + motion::slide(ui.ctx(), egui::Id::unique("sc_place_box"), target - at, DIM_S)
}
/// What the ring needs around it beyond its radius (pixels): the grip, the compass and the
+6 -1
View File
@@ -160,7 +160,12 @@ fn page_in_dir(app: &mut SolveApp, ui: &mut egui::Ui, dir: Option<&Path>) {
let searching = crate::prefs_search::querying(ui);
// The page's own box is for browsing Plugins. The dialog search is the filter while it is active.
if !searching {
crate::ui_kit::search_field(ui, ui.id().with("plugins_search"), &mut app.prefs_window.plugin_query, crate::i18n::tr("ui.plugins_search"));
crate::ui_kit::search_field(
ui,
ui.scope_id().with("plugins_search"),
&mut app.prefs_window.plugin_query,
crate::i18n::tr("ui.plugins_search"),
);
ui.add_space(8.0);
}
let (plugins, skipped) = scan(dir);
+11 -9
View File
@@ -459,9 +459,9 @@ fn page_path(index: usize) -> String {
fn search_header(ui: &mut egui::Ui, app: &mut SolveApp) {
let typed = drive_search(ui, app);
let field = ui_kit::preferences_search_field(ui, egui::Id::new("prefs_search"), &mut app.prefs_window.filter, crate::i18n::tr("prefs.search"));
let field = ui_kit::preferences_search_field(ui, egui::Id::unique("prefs_search"), &mut app.prefs_window.filter, crate::i18n::tr("prefs.search"));
if typed {
place_search_cursor(ui.ctx(), egui::Id::new("prefs_search"), app.prefs_window.filter.chars().count());
place_search_cursor(ui.ctx(), egui::Id::unique("prefs_search"), app.prefs_window.filter.chars().count());
}
crate::scenario::publish_handle("prefs:search", field.rect.center());
if field.has_focus() {
@@ -478,7 +478,7 @@ fn drive_search(ui: &mut egui::Ui, app: &mut SolveApp) -> bool {
if crate::keymap::capturing(app) {
return false;
}
let id = egui::Id::new("prefs_search");
let id = egui::Id::unique("prefs_search");
if ctx.input_mut(|input| input.consume_key(egui::Modifiers::COMMAND, egui::Key::F)) {
ctx.memory_mut(|memory| memory.request_focus(id));
}
@@ -2737,7 +2737,7 @@ impl SettingReset {
/// when a font was picked, and egui keeps that in the saved window state: it then opened as its
/// title bar alone, for good (#1800). Unfold it.
fn unfold(ctx: &egui::Context) {
if let Some(mut body) = egui::collapsing_header::CollapsingState::load(ctx, egui::Id::new("sc_prefs").with("collapsing"))
if let Some(mut body) = egui::collapsing_header::CollapsingState::load(ctx, egui::Id::unique("sc_prefs").with("collapsing"))
&& !body.is_open()
{
body.set_open(true);
@@ -2782,7 +2782,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let before = app.preferences.clone();
let capturing = crate::keymap::capturing(app);
let pending = apply_pending(app);
let window = ui_kit::Window::new(egui::Id::new("sc_prefs"), "settings", app.tr("ui.preferences"))
let window = ui_kit::Window::new(egui::Id::unique("sc_prefs"), "settings", app.tr("ui.preferences"))
.cards(10.0 / 3.0)
.page_rows(24.0)
.page_header_in_titlebar()
@@ -3198,7 +3198,7 @@ fn button_job_picker(
[None, Some(ButtonJob::Settings)].into_iter().chain(views.into_iter().chain(keys).chain(commands).map(Some)).collect();
let entries: Vec<(String, String)> = jobs.iter().map(|job| (button_job_value(job.as_ref()), button_job_label(app, job.as_ref()))).collect();
let options: Vec<_> = entries.iter().map(|(value, label)| ui_kit::SearchOption { value, label, handle: None }).collect();
let filter_id = egui::Id::new(("sc_space_mouse_button_filter", number));
let filter_id = egui::Id::unique(("sc_space_mouse_button_filter", number));
let mut filter: String = ui.data_mut(|data| data.get_temp(filter_id).unwrap_or_default());
let id = format!("sc_space_mouse_button_{number}");
let chosen = ui_kit::searchable_dropdown(ui, &id, &button_job_label(app, current), &mut filter, &options, None, None);
@@ -5017,7 +5017,7 @@ mod tests {
assert!(h.app.space_mouse.calibrating.is_some() && h.app.prefs_window.open);
let window = crate::scenario::rect_of("dialog:window").expect("the calibration window is the last window drawn");
let top = h.ctx.layer_id_at(window.center());
assert!(top.is_some_and(|layer| layer.id == egui::Id::new("sc_space_mouse_calibration")), "top is {top:?}");
assert!(top.is_some_and(|layer| layer.id == egui::Id::unique("sc_space_mouse_calibration")), "top is {top:?}");
}
/// Scrolls vertically to the handle, then clicks after the wheel animation settles.
@@ -5040,6 +5040,7 @@ mod tests {
}
h.app.synthetic.push(egui::Event::PointerMoved(body.center()));
h.app.synthetic.push(egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, if target.is_some_and(|at| at.y < body.top()) { 60.0 } else { -60.0 }),
modifiers: egui::Modifiers::NONE,
@@ -6681,10 +6682,11 @@ mod tests {
..Default::default()
};
let app = &mut h.app;
let out = h.ctx.run_ui(raw, |ui| {
let mut out = h.ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
let commands = out.viewport_output.get(&egui::ViewportId::ROOT).map(|v| v.commands.clone()).unwrap_or_default();
commands.into_iter().filter_map(|c| if let egui::ViewportCommand::SetTheme(t) = c { Some(t) } else { None }).collect::<Vec<_>>()
};
@@ -8282,7 +8284,7 @@ mod tests {
fn picking_a_font_keeps_the_preferences_window_unfolded() {
use egui::collapsing_header::CollapsingState;
use serde_json::json;
let body = egui::Id::new("sc_prefs").with("collapsing");
let body = egui::Id::unique("sc_prefs").with("collapsing");
let folded = |h: &crate::scenario::Harness| CollapsingState::load(&h.ctx, body).is_some_and(|s| !s.is_open());
let mut h = crate::scenario::Harness::new();
h.call("ui.prefs", json!({"open":"Interface"}));
+1 -1
View File
@@ -216,7 +216,7 @@ fn axes(app: &mut SolveApp, ui: &mut egui::Ui) {
let enabled = app.preferences.space_mouse_enabled;
let raw: [i16; 6] = app.space_mouse.raw.into();
// Frames come on input, so a reading that moved since the last one gets a frame to show it.
let seen = ui.id().with("space_mouse_meter_raw");
let seen = ui.scope_id().with("space_mouse_meter_raw");
let before = ui.data_mut(|data| data.get_temp::<[i16; 6]>(seen));
ui.data_mut(|data| data.insert_temp(seen, raw));
if before.is_some_and(|before| before != raw) {
+3 -3
View File
@@ -245,8 +245,8 @@ fn slot_count(app: &SolveApp, ui: &mut egui::Ui, stage: Rect, bar: Rect) {
};
let rect = Rect::from_min_size(egui::pos2(x, y), size);
ui.painter().with_clip_rect(stage).galley(rect.min, galley, view.text_dim);
let response = ui.interact(rect, ui.id().with("navbar_slot_count"), Sense::hover());
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), text.as_str()));
let response = ui.interact(rect, ui.scope_id().with("navbar_slot_count"), Sense::hover());
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Label, ui.is_enabled(), text.as_str()));
scenario::publish_rect("prefs:navbar:count", rect);
}
@@ -393,7 +393,7 @@ fn inspector_header(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, in
})
.inner
.backing_on_hover_text(label);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled && ui.is_enabled(), label));
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled && ui.is_enabled(), label));
scenario::publish_target(&format!("prefs:navbar:{key}"), response.rect);
if response.clicked() {
edit(app, state, index, key);
+1 -1
View File
@@ -10,7 +10,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if app.settings_reset.is_empty() {
return;
}
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_settings_reset"), "settings", app.tr("settings.reset_title"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_settings_reset"), "settings", app.tr("settings.reset_title"))
.actions(app.tr("ui.open_preferences"), app.tr("ui.heading_ok"), None)
.show(ctx, true, |ui| {
ui_kit::text(ui, app.tr("settings.reset_intro"), ui_kit::Text::Report("intro", false));
+19 -18
View File
@@ -100,7 +100,7 @@ impl Drop for SearchFrame {
pub(crate) fn begin(ui: &Ui, filter: &str) -> SearchFrame {
let query: String = filter.trim().chars().take(256).collect::<String>().to_lowercase();
ui.ctx().data_mut(|data| {
let state = data.get_temp_mut_or_insert_with(Id::new(STATE), State::default);
let state = data.get_temp_mut_or_insert_with(Id::unique(STATE), State::default);
let changed = state.memory_query != query;
if changed {
state.memory_query = query.clone();
@@ -131,7 +131,7 @@ pub(crate) fn begin(ui: &Ui, filter: &str) -> SearchFrame {
fn finish(ctx: &Context) -> bool {
let mut dirty = false;
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<State>(Id::new(STATE));
let state = data.get_temp_mut_or_default::<State>(Id::unique(STATE));
dirty = state.live && state.dirty;
state.live = false;
});
@@ -141,7 +141,7 @@ fn finish(ctx: &Context) -> bool {
fn with<R>(ui: &Ui, f: impl FnOnce(&mut State) -> R) -> Option<R> {
let mut out = None;
ui.ctx().data_mut(|data| {
let state = data.get_temp_mut_or_default::<State>(Id::new(STATE));
let state = data.get_temp_mut_or_default::<State>(Id::unique(STATE));
if state.live {
out = Some(f(state));
}
@@ -251,11 +251,11 @@ pub(crate) fn pop_passthrough(ui: &Ui) {
}
pub(crate) fn set_force_scroll(ctx: &Context, on: bool) {
ctx.data_mut(|data| data.insert_temp(Id::new(SCROLL), on));
ctx.data_mut(|data| data.insert_temp(Id::unique(SCROLL), on));
}
pub(crate) fn force_scroll(ctx: &Context) -> bool {
ctx.data(|data| data.get_temp(Id::new(SCROLL))).unwrap_or(false)
ctx.data(|data| data.get_temp(Id::unique(SCROLL))).unwrap_or(false)
}
pub(crate) fn begin_page(ui: &Ui, index: usize, label: &str) {
@@ -726,34 +726,34 @@ fn english_hit(query: &str, shown: &str) -> bool {
/// A text field or drag value had the keyboard on the previous frame.
pub(crate) fn shift_key_sink(ctx: &Context) -> bool {
let held = ctx.data(|data| data.get_temp::<bool>(Id::new(SINK_NOW))).unwrap_or(false);
ctx.data_mut(|data| data.insert_temp(Id::new(SINK_NOW), false));
let held = ctx.data(|data| data.get_temp::<bool>(Id::unique(SINK_NOW))).unwrap_or(false);
ctx.data_mut(|data| data.insert_temp(Id::unique(SINK_NOW), false));
held
}
pub(crate) fn key_sink(ctx: &Context) -> bool {
ctx.data(|data| data.get_temp::<bool>(Id::new(SINK_PREV))).unwrap_or(false)
ctx.data(|data| data.get_temp::<bool>(Id::unique(SINK_PREV))).unwrap_or(false)
}
pub(crate) fn note_key_sink(ctx: &Context) {
ctx.data_mut(|data| data.insert_temp(Id::new(SINK_NOW), true));
ctx.data_mut(|data| data.insert_temp(Id::unique(SINK_NOW), true));
}
pub(crate) fn set_key_sink_prev(ctx: &Context, held: bool) {
ctx.data_mut(|data| data.insert_temp(Id::new(SINK_PREV), held));
ctx.data_mut(|data| data.insert_temp(Id::unique(SINK_PREV), held));
}
/// The matches drawn last frame, for Up/Down and Enter before this pass rebuilds them.
pub(crate) fn nav(ctx: &Context) -> (String, usize, Vec<NavHit>) {
ctx.data(|data| {
let store = data.get_temp::<NavStore>(Id::new(NAV)).unwrap_or_default();
let store = data.get_temp::<NavStore>(Id::unique(NAV)).unwrap_or_default();
(store.query, store.cursor, store.hits)
})
}
pub(crate) fn set_cursor(ctx: &Context, cursor: usize) {
ctx.data_mut(|data| {
let store = data.get_temp_mut_or_default::<NavStore>(Id::new(NAV));
let store = data.get_temp_mut_or_default::<NavStore>(Id::unique(NAV));
store.scroll = store.cursor != cursor;
store.cursor = cursor;
});
@@ -765,7 +765,7 @@ pub(crate) fn store_nav(ui: &Ui) {
with(ui, |state| (state.query.clone(), state.nav.iter().map(|hit| NavHit { page: hit.page, label: hit.label.clone() }).collect::<Vec<_>>()))
.unwrap_or_default();
ui.ctx().data_mut(|data| {
let store = data.get_temp_mut_or_default::<NavStore>(Id::new(NAV));
let store = data.get_temp_mut_or_default::<NavStore>(Id::unique(NAV));
store.query = query;
store.hits = hits;
if store.cursor >= store.hits.len() {
@@ -783,7 +783,7 @@ pub(crate) fn remember(ui: &mut Ui, label: &str, rect: Rect) {
return;
}
let (cursor, scroll) = ui.ctx().data(|data| {
let store = data.get_temp::<NavStore>(Id::new(NAV)).unwrap_or_default();
let store = data.get_temp::<NavStore>(Id::unique(NAV)).unwrap_or_default();
(store.cursor, store.scroll)
});
let selected = with(ui, |state| {
@@ -807,9 +807,9 @@ pub(crate) fn remember(ui: &mut Ui, label: &str, rect: Rect) {
pub(crate) fn set_reveal(ctx: &Context, reveal: Option<Reveal>) {
ctx.data_mut(|data| {
if let Some(reveal) = reveal {
data.insert_temp(Id::new(REVEAL_ID), reveal);
data.insert_temp(Id::unique(REVEAL_ID), reveal);
} else {
data.remove::<Reveal>(Id::new(REVEAL_ID));
data.remove::<Reveal>(Id::unique(REVEAL_ID));
}
});
}
@@ -820,7 +820,7 @@ pub(crate) fn reveal_for(page: usize, label: &str) -> Reveal {
/// Scroll the opened setting into view and outline it until [`Reveal::until`].
pub(crate) fn follow(ui: &Ui, label: &str, rect: Rect) {
let Some(reveal) = ui.data(|data| data.get_temp::<Reveal>(Id::new(REVEAL_ID))) else { return };
let Some(reveal) = ui.data(|data| data.get_temp::<Reveal>(Id::unique(REVEAL_ID))) else { return };
if crate::now_ms() > reveal.until || fold(label) != fold(&reveal.label) {
return;
}
@@ -1213,7 +1213,7 @@ mod tests {
h.frames(2);
h.step(&serde_json::json!({"key": "F", "cmd": true})).expect("find");
h.frames(1);
assert!(h.ctx.memory(|memory| memory.has_focus(egui::Id::new("prefs_search"))), "Cmd+F focuses search");
assert!(h.ctx.memory(|memory| memory.has_focus(egui::Id::unique("prefs_search"))), "Cmd+F focuses search");
h.step(&serde_json::json!({"text": "zo"})).expect("type");
h.frames(20);
assert!(h.widget("Reverse zoom direction").is_some(), "typed letters search from the open page");
@@ -1402,6 +1402,7 @@ mod tests {
h.frames(20);
let first = crate::scenario::handle_at("prefs:search:current").expect("first match");
let wheel = egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -400.0),
modifiers: egui::Modifiers::NONE,
+1 -1
View File
@@ -252,7 +252,7 @@ pub(crate) fn show(app: &mut SolveApp, ui: &egui::Ui, view: egui::Rect, taken: &
return None;
}
let response =
ui.interact(r, egui::Id::new("sc_quick_measure"), egui::Sense::click()).backing_on_hover_text(crate::i18n::tr("measure.quick_copy"));
ui.interact(r, egui::Id::unique("sc_quick_measure"), egui::Sense::click()).backing_on_hover_text(crate::i18n::tr("measure.quick_copy"));
crate::scenario::publish_rect("measure:quick", r);
painter.rect_filled(r, t.radius_sm, if response.hovered() { t.hover } else { t.overlay });
painter.rect_stroke(r, t.radius_sm, egui::Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
+1 -1
View File
@@ -14,7 +14,7 @@ struct RecentColors {
}
fn recent_id() -> egui::Id {
egui::Id::new("color_recent_colors")
egui::Id::unique("color_recent_colors")
}
/// The recently picked colours, newest first.
+3 -3
View File
@@ -140,7 +140,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
let mut act: Option<(Entry, bool)> = None;
let mut discard_all = false;
let (closed, action) = ui_kit::Window::new(egui::Id::new("sc_recover_prompt"), "restore", crate::i18n::tr_label("Recover unsaved design?"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_recover_prompt"), "restore", crate::i18n::tr_label("Recover unsaved design?"))
.actions("", crate::i18n::tr_label("Later"), None)
.show_with_left(
ctx,
@@ -216,7 +216,7 @@ pub fn browser_copies_window(ctx: &egui::Context, language: &str, copies: &[Brow
_ => crate::i18n::tr("recovery.restore_title").to_string(),
};
let mut act = None;
let _ = ui_kit::Window::new(egui::Id::new("sc_browser_keep_restore"), "restore", &title).required().show_body(ctx, |ui| {
let _ = ui_kit::Window::new(egui::Id::unique("sc_browser_keep_restore"), "restore", &title).required().show_body(ctx, |ui| {
ui_kit::text(ui, crate::i18n::tr("recovery.restore_intro"), ui_kit::Text::Report("intro", false));
for (index, c) in copies.iter().enumerate() {
if copies.len() > 1 {
@@ -249,7 +249,7 @@ fn confirm_discard_all(app: &mut SolveApp, ctx: &egui::Context) {
} else {
crate::i18n::format("recovery.discard_all_title", &[("count", &count.to_string())])
};
let (closed, action) = ui_kit::Confirm::new(egui::Id::new("sc_recover_discard_all"), "delete", &title)
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_recover_discard_all"), "delete", &title)
.tone(ui_kit::Tone::Danger)
.actions(crate::i18n::tr("recovery.discard_all"), crate::i18n::tr("ui.cancel"), None)
.show(ctx, true, |ui| {
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff. Show more