mirror of
https://github.com/danbulant/cushy
synced 2026-06-17 21:41:11 +00:00
Merge branch 'main' into feature/world-coords
This commit is contained in:
commit
a8841e6f1c
56 changed files with 3292 additions and 1001 deletions
|
|
@ -16,16 +16,20 @@ reactive data models work, consider this example that displays a button that
|
|||
increments its own label:
|
||||
|
||||
```rust,ignore
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_usize);
|
||||
fn main() -> gooey::Result {
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_isize);
|
||||
// Create a dynamic that contains `count.to_string()`
|
||||
let count_label = count.map_each(ToString::to_string);
|
||||
|
||||
// Create a new button with a label that is produced by mapping the contents
|
||||
// of `count`.
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
// Run the button as an an application.
|
||||
.run()
|
||||
// Create a new button whose text is our dynamic string.
|
||||
count_label
|
||||
.into_button()
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
// Run the application
|
||||
.run()
|
||||
}
|
||||
```
|
||||
|
||||
[widget]: crate::widget::Widget
|
||||
|
|
@ -33,7 +37,7 @@ Button::new(count.map_each(ToString::to_string))
|
|||
[wgpu]: https://github.com/gfx-rs/wgpu
|
||||
[winit]: https://github.com/rust-windowing/winit
|
||||
[widgets]: mod@crate::widgets
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/button.rs
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/basic-button.rs
|
||||
|
||||
## Open-source Licenses
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ reactive data models work, consider this example that displays a button that
|
|||
increments its own label:
|
||||
|
||||
```rust,ignore
|
||||
$../examples/button.rs:readme$
|
||||
$../examples/basic-button.rs:readme$
|
||||
```
|
||||
|
||||
[widget]: $widget$
|
||||
|
|
@ -24,4 +24,4 @@ $../examples/button.rs:readme$
|
|||
[wgpu]: https://github.com/gfx-rs/wgpu
|
||||
[winit]: https://github.com/rust-windowing/winit
|
||||
[widgets]: $widgets$
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/button.rs
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/basic-button.rs
|
||||
|
|
|
|||
266
Cargo.lock
generated
266
Cargo.lock
generated
|
|
@ -121,6 +121,25 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics",
|
||||
"image",
|
||||
"log",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"parking_lot",
|
||||
"thiserror",
|
||||
"winapi",
|
||||
"x11rb 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
|
|
@ -309,10 +328,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.84"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -334,6 +354,17 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "4.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
"str-buf",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
|
|
@ -445,6 +476,15 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursor-icon"
|
||||
version = "1.1.0"
|
||||
|
|
@ -473,6 +513,26 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0-beta.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7abbfc297053be59290e3152f8cbcd52c8642e0728b69ee187d991d4c1af08d"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0-beta.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
|
|
@ -508,14 +568,24 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
|||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
|
||||
checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"str-buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etagere"
|
||||
version = "0.2.8"
|
||||
|
|
@ -541,10 +611,19 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
|
||||
source = "git+https://github.com/khonsulabs/figures#52d06f3623cdb47128f1537fdadfe190f7afa88e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
@ -553,6 +632,16 @@ dependencies = [
|
|||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float_next_after"
|
||||
version = "1.0.0"
|
||||
|
|
@ -654,6 +743,16 @@ version = "0.3.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.3.0"
|
||||
|
|
@ -721,6 +820,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
"arboard",
|
||||
"derive_more",
|
||||
"gooey-macros",
|
||||
"intentional",
|
||||
"interner",
|
||||
|
|
@ -738,7 +839,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"attribute-derive",
|
||||
"insta",
|
||||
"manyhow 0.9.0",
|
||||
"manyhow 0.10.0",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -858,6 +959,8 @@ dependencies = [
|
|||
"color_quant",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"png",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -934,6 +1037,21 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
|
|
@ -969,7 +1087,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#09790aafb5a9c3b0da034387adead9960eb06bc7"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#ec57aacb2ac1df099ac0abd5f0848a01ce843225"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -983,6 +1101,7 @@ dependencies = [
|
|||
"lyon_tessellation",
|
||||
"pollster",
|
||||
"smallvec",
|
||||
"unicode-bidi",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
|
|
@ -1118,11 +1237,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "manyhow"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aebef87880bafc898c6bed1435e8fdc58634275ff97693a4bb96ad561c73c43"
|
||||
checksum = "4efde575f79afb9c637eb4663aa451f0bf227413aa734fbbec077cab5900be85"
|
||||
dependencies = [
|
||||
"manyhow-macros 0.9.0",
|
||||
"manyhow-macros 0.10.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
|
|
@ -1141,9 +1260,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "manyhow-macros"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f74cc8a0d8b05a7e919011c78a2744e7dea66567c05fb046666f3bae383d8d04"
|
||||
checksum = "fcee04599474650eb26ae5a5c7837e30e55242267ff1bf0adc760b6fcdc3fa2a"
|
||||
dependencies = [
|
||||
"proc-macro-utils",
|
||||
"proc-macro2",
|
||||
|
|
@ -1229,13 +1348,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "0.14.0"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61d829abac9f5230a85d8cc83ec0879b4c09790208ae25b5ea031ef84562e071"
|
||||
checksum = "6cd05939c491da968a42986204b7431678be21fdcd4b10cc84997ba130ada5a4"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags 2.4.1",
|
||||
|
|
@ -1290,6 +1410,18 @@ dependencies = [
|
|||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.25.1"
|
||||
|
|
@ -1397,6 +1529,17 @@ dependencies = [
|
|||
"objc_exception",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.1"
|
||||
|
|
@ -1428,6 +1571,15 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_id"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||
dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.1"
|
||||
|
|
@ -1579,6 +1731,19 @@ version = "0.3.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1807,9 +1972,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.21"
|
||||
version = "0.38.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
|
|
@ -1904,6 +2069,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.3.0"
|
||||
|
|
@ -1987,6 +2158,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "str-buf"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2068,6 +2245,17 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.2"
|
||||
|
|
@ -2465,6 +2653,12 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "wgpu"
|
||||
version = "0.18.0"
|
||||
|
|
@ -2492,9 +2686,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "0.18.0"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "837e02ddcdc6d4a9b56ba4598f7fd4202a7699ab03f6ef4dcdebfad2c966aea6"
|
||||
checksum = "ef91c1d62d1e9e81c79e600131a258edf75c9531cbdbde09c44a011a47312726"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-vec",
|
||||
|
|
@ -2807,7 +3001,7 @@ dependencies = [
|
|||
"web-time",
|
||||
"windows-sys 0.48.0",
|
||||
"x11-dl",
|
||||
"x11rb",
|
||||
"x11rb 0.12.0",
|
||||
"xkbcommon-dl",
|
||||
]
|
||||
|
||||
|
|
@ -2831,6 +3025,19 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507"
|
||||
dependencies = [
|
||||
"gethostname 0.2.3",
|
||||
"nix 0.24.3",
|
||||
"winapi",
|
||||
"winapi-wsapoll",
|
||||
"x11rb-protocol 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.12.0"
|
||||
|
|
@ -2838,14 +3045,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"gethostname",
|
||||
"gethostname 0.3.0",
|
||||
"libc",
|
||||
"libloading 0.7.4",
|
||||
"nix 0.26.4",
|
||||
"once_cell",
|
||||
"winapi",
|
||||
"winapi-wsapoll",
|
||||
"x11rb-protocol",
|
||||
"x11rb-protocol 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67"
|
||||
dependencies = [
|
||||
"nix 0.24.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2920,18 +3136,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
|
||||
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.25"
|
||||
version = "0.7.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
|
||||
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ tracing-subscriber = { version = "0.3", optional = true, features = [
|
|||
palette = "0.7.3"
|
||||
ahash = "0.8.6"
|
||||
gooey-macros = { version = "0.1.0", path = "gooey-macros" }
|
||||
derive_more = { version = "1.0.0-beta.6", features = ["from"] }
|
||||
arboard = "3.2.1"
|
||||
|
||||
|
||||
# [patch."https://github.com/khonsulabs/kludgine"]
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -18,16 +18,20 @@ reactive data models work, consider this example that displays a button that
|
|||
increments its own label:
|
||||
|
||||
```rust,ignore
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_usize);
|
||||
fn main() -> gooey::Result {
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_isize);
|
||||
// Create a dynamic that contains `count.to_string()`
|
||||
let count_label = count.map_each(ToString::to_string);
|
||||
|
||||
// Create a new button with a label that is produced by mapping the contents
|
||||
// of `count`.
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
// Run the button as an an application.
|
||||
.run()
|
||||
// Create a new button whose text is our dynamic string.
|
||||
count_label
|
||||
.into_button()
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
// Run the application
|
||||
.run()
|
||||
}
|
||||
```
|
||||
|
||||
[widget]: https://gooey.rs/main/gooey/widget/trait.Widget.html
|
||||
|
|
@ -35,7 +39,7 @@ Button::new(count.map_each(ToString::to_string))
|
|||
[wgpu]: https://github.com/gfx-rs/wgpu
|
||||
[winit]: https://github.com/rust-windowing/winit
|
||||
[widgets]: https://gooey.rs/main/gooey/widgets/index.html
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/button.rs
|
||||
[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/basic-button.rs
|
||||
|
||||
## Open-source Licenses
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use std::time::Duration;
|
|||
use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn};
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Button, Label, Stack};
|
||||
use gooey::{Run, WithClone};
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
|
|
@ -18,13 +17,17 @@ fn main() -> gooey::Result {
|
|||
.on_complete(|| println!("Gooey animations are neat!"))
|
||||
.launch();
|
||||
|
||||
Stack::columns(
|
||||
Button::new("To 0")
|
||||
.on_click(animate_to(&animation, &value, 0))
|
||||
.and(Label::new(label))
|
||||
.and(Button::new("To 100").on_click(animate_to(&animation, &value, 100))),
|
||||
)
|
||||
.run()
|
||||
"To 0"
|
||||
.into_button()
|
||||
.on_click(animate_to(&animation, &value, 0))
|
||||
.and(label)
|
||||
.and(
|
||||
"To 100"
|
||||
.into_button()
|
||||
.on_click(animate_to(&animation, &value, 100)),
|
||||
)
|
||||
.into_columns()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn animate_to(
|
||||
|
|
|
|||
20
examples/basic-button.rs
Normal file
20
examples/basic-button.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::Run;
|
||||
|
||||
// begin rustme snippet: readme
|
||||
fn main() -> gooey::Result {
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_isize);
|
||||
// Create a dynamic that contains `count.to_string()`
|
||||
let count_label = count.map_each(ToString::to_string);
|
||||
|
||||
// Create a new button whose text is our dynamic string.
|
||||
count_label
|
||||
.into_button()
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
// Run the application
|
||||
.run()
|
||||
}
|
||||
// end rustme snippet
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::button::ButtonOutline;
|
||||
use gooey::widgets::Button;
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
||||
// begin rustme snippet: readme
|
||||
fn main() -> gooey::Result {
|
||||
// Create a dynamic usize.
|
||||
let count = Dynamic::new(0_isize);
|
||||
|
||||
// Create a new button with a label that is produced by mapping the contents
|
||||
// of `count`.
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that increments the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
|
||||
.and(
|
||||
// Creates a second, outlined button
|
||||
Button::new(count.map_each(ToString::to_string))
|
||||
// Set the `on_click` callback to a closure that decrements the counter.
|
||||
.on_click(count.with_clone(|count| move |_| count.set(count.get() - 1)))
|
||||
.with(&ButtonOutline, Color::DARKRED),
|
||||
)
|
||||
.into_columns()
|
||||
// Run the button as an an application.
|
||||
.run()
|
||||
}
|
||||
// end rustme snippet
|
||||
56
examples/buttons.rs
Normal file
56
examples/buttons.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::button::ButtonKind;
|
||||
use gooey::widgets::{Button, Checkbox};
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let clicked_label = Dynamic::new(String::from("Click a Button"));
|
||||
let default_is_outline = Dynamic::new(false);
|
||||
let default_button_style = default_is_outline.map_each(|is_outline| {
|
||||
if *is_outline {
|
||||
ButtonKind::Outline
|
||||
} else {
|
||||
ButtonKind::Solid
|
||||
}
|
||||
});
|
||||
|
||||
clicked_label
|
||||
.clone()
|
||||
.and(
|
||||
Button::new("Normal Button")
|
||||
.on_click(
|
||||
clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Normal Button"))
|
||||
}),
|
||||
)
|
||||
.and(
|
||||
Button::new("Outline Button")
|
||||
.on_click(clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Outline Button"))
|
||||
}))
|
||||
.kind(ButtonKind::Outline),
|
||||
)
|
||||
.and(
|
||||
Button::new("Transparent Button")
|
||||
.on_click(clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Transparent Button"))
|
||||
}))
|
||||
.kind(ButtonKind::Transparent),
|
||||
)
|
||||
.and(
|
||||
Button::new("Default Button")
|
||||
.on_click(clicked_label.with_clone(|label| {
|
||||
move |_| label.set(String::from("Clicked Default Button"))
|
||||
}))
|
||||
.kind(default_button_style)
|
||||
.into_default(),
|
||||
)
|
||||
.and(Checkbox::new(default_is_outline, "Set Default to Outline"))
|
||||
.into_columns(),
|
||||
)
|
||||
.into_rows()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
19
examples/checkbox.rs
Normal file
19
examples/checkbox.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::checkbox::CheckboxState;
|
||||
use gooey::widgets::Checkbox;
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let checkbox_state = Dynamic::new(CheckboxState::Checked);
|
||||
let label = checkbox_state.map_each(|state| format!("Check Me! Current: {state:?}"));
|
||||
|
||||
Checkbox::new(checkbox_state.clone(), label)
|
||||
.and("Maybe".into_button().on_click(move |()| {
|
||||
checkbox_state.update(CheckboxState::Indeterminant);
|
||||
}))
|
||||
.into_columns()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::{MakeWidget, WidgetInstance};
|
||||
use gooey::widgets::{Button, Label};
|
||||
use gooey::window::ThemeMode;
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let theme_mode = Dynamic::default();
|
||||
set_of_containers(1, theme_mode.clone())
|
||||
set_of_containers(3, theme_mode.clone())
|
||||
.centered()
|
||||
.expand()
|
||||
.into_window()
|
||||
.with_theme_mode(theme_mode)
|
||||
.themed_mode(theme_mode)
|
||||
.run()
|
||||
}
|
||||
|
||||
|
|
@ -17,20 +17,21 @@ fn set_of_containers(repeat: usize, theme_mode: Dynamic<ThemeMode>) -> WidgetIns
|
|||
let inner = if let Some(remaining_iters) = repeat.checked_sub(1) {
|
||||
set_of_containers(remaining_iters, theme_mode)
|
||||
} else {
|
||||
Button::new("Toggle Theme Mode")
|
||||
"Toggle Theme Mode"
|
||||
.into_button()
|
||||
.on_click(move |_| {
|
||||
theme_mode.map_mut(|mode| mode.toggle());
|
||||
})
|
||||
.make_widget()
|
||||
};
|
||||
Label::new("Lowest")
|
||||
"Lowest"
|
||||
.and(
|
||||
Label::new("Low")
|
||||
"Low"
|
||||
.and(
|
||||
Label::new("Mid")
|
||||
"Mid"
|
||||
.and(
|
||||
Label::new("High")
|
||||
.and(Label::new("Highest").and(inner).into_rows().contain())
|
||||
"High"
|
||||
.and("Highest".and(inner).into_rows().contain())
|
||||
.into_rows()
|
||||
.contain(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::string::ToString;
|
|||
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Button, Label};
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
|
|
@ -10,14 +9,14 @@ fn main() -> gooey::Result {
|
|||
let counter = Dynamic::new(0i32);
|
||||
let label = counter.map_each(ToString::to_string);
|
||||
|
||||
Label::new(label)
|
||||
label
|
||||
.width(Lp::points(100))
|
||||
.and(Button::new("+").on_click(counter.with_clone(|counter| {
|
||||
.and("+".into_button().on_click(counter.with_clone(|counter| {
|
||||
move |_| {
|
||||
*counter.lock() += 1;
|
||||
}
|
||||
})))
|
||||
.and(Button::new("-").on_click(counter.with_clone(|counter| {
|
||||
.and("-".into_button().on_click(counter.with_clone(|counter| {
|
||||
move |_| {
|
||||
*counter.lock() -= 1;
|
||||
}
|
||||
|
|
|
|||
84
examples/focus-order.rs
Normal file
84
examples/focus-order.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::process::exit;
|
||||
|
||||
use gooey::value::{Dynamic, MapEach, StringValue};
|
||||
use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag};
|
||||
use gooey::widgets::Expand;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
/// This example is the same as login, but it has an explicit tab order to
|
||||
/// change from the default order (username, password, cancel, log in) to
|
||||
/// username, password, log in, cancel.
|
||||
fn main() -> gooey::Result {
|
||||
let username = Dynamic::default();
|
||||
let password = Dynamic::default();
|
||||
|
||||
let valid =
|
||||
(&username, &password).map_each(|(username, password)| validate(username, password));
|
||||
|
||||
let (login_tag, login_id) = WidgetTag::new();
|
||||
let (cancel_tag, cancel_id) = WidgetTag::new();
|
||||
let (username_tag, username_id) = WidgetTag::new();
|
||||
|
||||
// TODO this should be a grid layout to ensure proper visual alignment.
|
||||
let username_row = "Username"
|
||||
.and(
|
||||
username
|
||||
.clone()
|
||||
.into_input()
|
||||
.make_with_id(username_tag)
|
||||
.expand(),
|
||||
)
|
||||
.into_columns();
|
||||
|
||||
let password_row = "Password"
|
||||
.and(
|
||||
// TODO secure input
|
||||
password
|
||||
.clone()
|
||||
.into_input()
|
||||
.with_next_focus(login_id)
|
||||
.expand(),
|
||||
)
|
||||
.into_columns();
|
||||
|
||||
let buttons = "Cancel"
|
||||
.into_button()
|
||||
.on_click(|_| {
|
||||
eprintln!("Login cancelled");
|
||||
exit(0)
|
||||
})
|
||||
.make_with_id(cancel_tag)
|
||||
.into_escape()
|
||||
.with_next_focus(username_id)
|
||||
.and(Expand::empty())
|
||||
.and(
|
||||
"Log In"
|
||||
.into_button()
|
||||
.on_click(move |_| {
|
||||
println!("Welcome, {}", username.get());
|
||||
exit(0);
|
||||
})
|
||||
.make_with_id(login_tag)
|
||||
.with_enabled(valid)
|
||||
.into_default()
|
||||
.with_next_focus(cancel_id),
|
||||
)
|
||||
.into_columns();
|
||||
|
||||
username_row
|
||||
.pad()
|
||||
.and(password_row.pad())
|
||||
.and(buttons.pad())
|
||||
.into_rows()
|
||||
.contain()
|
||||
.width(Lp::points(300)..Lp::points(600))
|
||||
.scroll()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn validate(username: &String, password: &String) -> bool {
|
||||
!username.is_empty() && !password.is_empty()
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::value::{Dynamic, StringValue};
|
||||
use gooey::widget::{MakeWidget, HANDLED, IGNORED};
|
||||
use gooey::widgets::{Input, Label, Space};
|
||||
use gooey::widgets::Space;
|
||||
use gooey::Run;
|
||||
use kludgine::app::winit::event::ElementState;
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
|
|
@ -10,13 +10,14 @@ fn main() -> gooey::Result {
|
|||
let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100));
|
||||
let chat_message = Dynamic::new(String::new());
|
||||
|
||||
Label::new(chat_log.clone())
|
||||
chat_log
|
||||
.clone()
|
||||
.vertical_scroll()
|
||||
.expand()
|
||||
.and(Space::colored(Color::RED).expand_weighted(2))
|
||||
.into_columns()
|
||||
.expand()
|
||||
.and(Input::new(chat_message.clone()).on_key(move |input| {
|
||||
.and(chat_message.clone().into_input().on_key(move |input| {
|
||||
match (input.state, input.logical_key) {
|
||||
(ElementState::Pressed, Key::Enter) => {
|
||||
let new_message = chat_message.map_mut(std::mem::take);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use gooey::value::StringValue;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::Input;
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
Input::new("Hello").expand().run()
|
||||
"Hello".into_input().expand().run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::process::exit;
|
||||
|
||||
use gooey::value::{Dynamic, MapEach};
|
||||
use gooey::value::{Dynamic, MapEach, StringValue};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Button, Expand, Input, Label};
|
||||
use gooey::widgets::Expand;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
|
|
@ -14,18 +14,19 @@ fn main() -> gooey::Result {
|
|||
(&username, &password).map_each(|(username, password)| validate(username, password));
|
||||
|
||||
// TODO this should be a grid layout to ensure proper visual alignment.
|
||||
let username_row = Label::new("Username")
|
||||
.and(Input::new(username.clone()).expand())
|
||||
let username_row = "Username"
|
||||
.and(username.clone().into_input().expand())
|
||||
.into_columns();
|
||||
|
||||
let password_row = Label::new("Password")
|
||||
let password_row = "Password"
|
||||
.and(
|
||||
// TODO secure input
|
||||
Input::new(password.clone()).expand(),
|
||||
password.clone().into_input().expand(),
|
||||
)
|
||||
.into_columns();
|
||||
|
||||
let buttons = Button::new("Cancel")
|
||||
let buttons = "Cancel"
|
||||
.into_button()
|
||||
.on_click(|_| {
|
||||
eprintln!("Login cancelled");
|
||||
exit(0)
|
||||
|
|
@ -33,13 +34,14 @@ fn main() -> gooey::Result {
|
|||
.into_escape()
|
||||
.and(Expand::empty())
|
||||
.and(
|
||||
Button::new("Log In")
|
||||
.enabled(valid)
|
||||
"Log In"
|
||||
.into_button()
|
||||
.on_click(move |_| {
|
||||
println!("Welcome, {}", username.get());
|
||||
exit(0);
|
||||
})
|
||||
.into_default(),
|
||||
.into_default()
|
||||
.with_enabled(valid),
|
||||
)
|
||||
.into_columns();
|
||||
|
||||
|
|
|
|||
18
examples/nested-scroll.rs
Normal file
18
examples/nested-scroll.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use gooey::widget::MakeWidget;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
include_str!("./nested-scroll.rs")
|
||||
.vertical_scroll()
|
||||
.height(Lp::inches(3))
|
||||
.and(
|
||||
include_str!("./canvas.rs")
|
||||
.vertical_scroll()
|
||||
.height(Lp::inches(3)),
|
||||
)
|
||||
.into_rows()
|
||||
.vertical_scroll()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::Label;
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
Label::new(include_str!("../src/widgets/scroll.rs"))
|
||||
include_str!("../src/widgets/scroll.rs")
|
||||
.scroll()
|
||||
.expand()
|
||||
.run()
|
||||
|
|
|
|||
67
examples/slider.rs
Normal file
67
examples/slider.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use gooey::animation::{LinearInterpolate, PercentBetween};
|
||||
use gooey::value::{Dynamic, StringValue};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::slider::Slidable;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
use kludgine::figures::Ranged;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
u8_slider()
|
||||
.and(enum_slider())
|
||||
.into_rows()
|
||||
.expand_horizontally()
|
||||
.width(..Lp::points(800))
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn u8_slider() -> impl MakeWidget {
|
||||
let min_text = Dynamic::new(u8::MIN.to_string());
|
||||
let min = min_text.map_each(|min| min.parse().unwrap_or(u8::MIN));
|
||||
let max_text = Dynamic::new(u8::MAX.to_string());
|
||||
let max = max_text.map_each(|max| max.parse().unwrap_or(u8::MAX));
|
||||
let value = Dynamic::new(128_u8);
|
||||
let value_text = value.map_each(ToString::to_string);
|
||||
|
||||
"Min"
|
||||
.and(min_text.into_input())
|
||||
.and("Max")
|
||||
.and(max_text.into_input())
|
||||
.into_columns()
|
||||
.centered()
|
||||
.and(value.slider_between(min, max))
|
||||
.and(value_text.centered())
|
||||
.into_rows()
|
||||
}
|
||||
|
||||
#[derive(LinearInterpolate, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
enum SlidableEnum {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
}
|
||||
|
||||
impl PercentBetween for SlidableEnum {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> gooey::animation::ZeroToOne {
|
||||
let min = *min as u8;
|
||||
let max = *max as u8;
|
||||
let value = *self as u8;
|
||||
value.percent_between(&min, &max)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for SlidableEnum {
|
||||
const MAX: Self = Self::C;
|
||||
const MIN: Self = Self::A;
|
||||
}
|
||||
|
||||
fn enum_slider() -> impl MakeWidget {
|
||||
let enum_value = Dynamic::new(SlidableEnum::A);
|
||||
let enum_text = enum_value.map_each(|value| format!("{value:?}"));
|
||||
"Custom Enum"
|
||||
.and(enum_value.slider())
|
||||
.and(enum_text)
|
||||
.into_rows()
|
||||
}
|
||||
52
examples/stack-align-test.rs
Normal file
52
examples/stack-align-test.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use gooey::widget::MakeWidget;
|
||||
use gooey::Run;
|
||||
|
||||
/// This example shows a tricky layout problem. The hierarchy of widgets is
|
||||
/// this:
|
||||
///
|
||||
/// ```text
|
||||
/// Expand (.expand())
|
||||
/// | Align (.centered())
|
||||
/// | | Stack (.into_rows())
|
||||
/// | | | Label
|
||||
/// | | | Align (.centered())
|
||||
/// | | | | Button
|
||||
/// ```
|
||||
///
|
||||
/// When the Stack widget attempted to implmement a single-pass layout, this
|
||||
/// caused the Button to be aligned to the left inside of the stack. The Stack
|
||||
/// widget now utilizes two `layout()` operations for layouts like this. Here's
|
||||
/// the reasoning:
|
||||
///
|
||||
/// At the window root, we have an Align wrapped by an Expand. The Align widget
|
||||
/// during layout asks its children to size-to-fit. This means the Stack is
|
||||
/// asking its children to size-to-fit as well.
|
||||
///
|
||||
/// The Stack's orientation is Rows, and since the children are Resizes or
|
||||
/// Expands, the widgets are size-to-fit. This means that the Stack will measure
|
||||
/// these widgets asking them to size to fit.
|
||||
///
|
||||
/// After running this pass of measurement, we can assign the heights of each of
|
||||
/// the rows to the measurements we received. The width of the stack becomes the
|
||||
/// maximum width of all children measured.
|
||||
///
|
||||
/// In a single-pass layout, this means the Align widget inside of the Stack
|
||||
/// never receives an opportunity to lay its children out with the final width.
|
||||
/// The Button does end up centered because of this. Fixing it also becomes
|
||||
/// tricky, because if surround the button in an Expand, it now instructs the
|
||||
/// Stack to expand to fill its parent.
|
||||
///
|
||||
/// After some careful deliberation, @ecton reasoned that in the situation where
|
||||
/// a Stack is asked to layout with the Stack's non-primary being a size-to-fit
|
||||
/// measurement, a second layout call for all children is required with Known
|
||||
/// measurements to allow layouts like this example to work correctly.
|
||||
fn main() -> gooey::Result {
|
||||
// TODO once we have offscreen rendering, turn this into a test case
|
||||
"Really Long Label"
|
||||
.and("Short".into_button().centered())
|
||||
.into_rows()
|
||||
.contain()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use gooey::styles::components::TextColor;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::stack::Stack;
|
||||
use gooey::widgets::{Button, Style};
|
||||
use gooey::widgets::Style;
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
Stack::rows(Button::new("Green").and(red_text(Button::new("Red"))))
|
||||
Stack::rows("Green".and(red_text("Red")))
|
||||
.with(&TextColor, Color::GREEN)
|
||||
.run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::value::{Dynamic, Switchable};
|
||||
use gooey::widget::{MakeWidget, WidgetInstance};
|
||||
use gooey::widgets::{Button, Label, Switcher};
|
||||
use gooey::Run;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -12,21 +11,23 @@ enum ActiveContent {
|
|||
fn main() -> gooey::Result {
|
||||
let active = Dynamic::new(ActiveContent::Intro);
|
||||
|
||||
Switcher::new(active.clone(), move |content| match content {
|
||||
ActiveContent::Intro => intro(active.clone()),
|
||||
ActiveContent::Success => success(active.clone()),
|
||||
})
|
||||
.contain()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
active
|
||||
.switcher(|current, active| match current {
|
||||
ActiveContent::Intro => intro(active.clone()),
|
||||
ActiveContent::Success => success(active.clone()),
|
||||
})
|
||||
.contain()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance {
|
||||
const INTRO: &str = "This example demonstrates the Switcher<T> widget, which uses a mapping function to convert from a generic type to the widget it uses for its contents.";
|
||||
Label::new(INTRO)
|
||||
INTRO
|
||||
.and(
|
||||
Button::new("Switch!")
|
||||
"Switch!"
|
||||
.into_button()
|
||||
.on_click(move |_| active.set(ActiveContent::Success))
|
||||
.centered(),
|
||||
)
|
||||
|
|
@ -35,11 +36,12 @@ fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance {
|
|||
}
|
||||
|
||||
fn success(active: Dynamic<ActiveContent>) -> WidgetInstance {
|
||||
Label::new("The value changed to `ActiveContent::Success`!")
|
||||
"The value changed to `ActiveContent::Success`!"
|
||||
.and(
|
||||
Button::new("Start Over")
|
||||
"Start Over"
|
||||
.into_button()
|
||||
.on_click(move |_| active.set(ActiveContent::Intro))
|
||||
// .centered(),
|
||||
.centered(),
|
||||
)
|
||||
.into_rows()
|
||||
.make_widget()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use gooey::animation::ZeroToOne;
|
||||
use gooey::styles::components::{TextColor, WidgetBackground};
|
||||
use gooey::styles::{
|
||||
ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair,
|
||||
};
|
||||
use gooey::value::{Dynamic, MapEach};
|
||||
use gooey::value::{Dynamic, MapEach, StringValue};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Input, Label, ModeSwitch, Scroll, Slider, Stack, Themed};
|
||||
use gooey::widgets::slider::Slidable;
|
||||
use gooey::widgets::{Slider, Stack};
|
||||
use gooey::window::ThemeMode;
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
|
@ -44,38 +43,37 @@ fn main() -> gooey::Result {
|
|||
},
|
||||
);
|
||||
|
||||
Themed::new(
|
||||
default_theme.clone(),
|
||||
Stack::columns(
|
||||
Scroll::vertical(Stack::rows(
|
||||
theme_switcher
|
||||
.and(primary_editor)
|
||||
.and(secondary_editor)
|
||||
.and(tertiary_editor)
|
||||
.and(error_editor)
|
||||
.and(neutral_editor)
|
||||
.and(neutral_variant_editor),
|
||||
))
|
||||
.and(fixed_themes(
|
||||
default_theme.map_each(|theme| theme.primary_fixed),
|
||||
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||
))
|
||||
.and(theme(
|
||||
default_theme.map_each(|theme| theme.dark),
|
||||
ThemeMode::Dark,
|
||||
))
|
||||
.and(theme(
|
||||
default_theme.map_each(|theme| theme.light),
|
||||
ThemeMode::Light,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.pad()
|
||||
.expand()
|
||||
.into_window()
|
||||
.with_theme_mode(theme_mode)
|
||||
.run()
|
||||
let editors = theme_switcher
|
||||
.and(primary_editor)
|
||||
.and(secondary_editor)
|
||||
.and(tertiary_editor)
|
||||
.and(error_editor)
|
||||
.and(neutral_editor)
|
||||
.and(neutral_variant_editor)
|
||||
.into_rows()
|
||||
.vertical_scroll();
|
||||
|
||||
editors
|
||||
.and(fixed_themes(
|
||||
default_theme.map_each(|theme| theme.primary_fixed),
|
||||
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||
))
|
||||
.and(theme(
|
||||
default_theme.map_each(|theme| theme.dark),
|
||||
ThemeMode::Dark,
|
||||
))
|
||||
.and(theme(
|
||||
default_theme.map_each(|theme| theme.light),
|
||||
ThemeMode::Light,
|
||||
))
|
||||
.into_columns()
|
||||
.themed(default_theme)
|
||||
.pad()
|
||||
.expand()
|
||||
.into_window()
|
||||
.themed_mode(theme_mode)
|
||||
.run()
|
||||
}
|
||||
|
||||
fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
||||
|
|
@ -83,30 +81,18 @@ fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
|||
|
||||
(
|
||||
theme_mode.clone(),
|
||||
Stack::rows(Label::new("Theme Mode").and(Slider::<ThemeMode>::from_value(theme_mode))),
|
||||
"Theme Mode".and(theme_mode.slider()).into_rows(),
|
||||
)
|
||||
}
|
||||
|
||||
fn create_paired_string<T>(initial_value: T) -> (Dynamic<T>, Dynamic<String>)
|
||||
where
|
||||
T: ToString + PartialEq + FromStr + Default + Send + Sync + 'static,
|
||||
{
|
||||
let float = Dynamic::new(initial_value);
|
||||
let text = float.map_each_unique(|f| f.to_string());
|
||||
text.for_each(float.with_clone(|float| {
|
||||
move |text: &String| {
|
||||
let _result = float.try_update(text.parse().unwrap_or_default());
|
||||
}
|
||||
}));
|
||||
(float, text)
|
||||
}
|
||||
|
||||
fn color_editor(
|
||||
initial_color: ColorSource,
|
||||
label: &str,
|
||||
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
||||
let (hue, hue_text) = create_paired_string(initial_color.hue.into_degrees());
|
||||
let (saturation, saturation_text) = create_paired_string(initial_color.saturation);
|
||||
let hue = Dynamic::new(initial_color.hue.into_degrees());
|
||||
let hue_text = hue.linked_string();
|
||||
let saturation = Dynamic::new(initial_color.saturation);
|
||||
let saturation_text = saturation.linked_string();
|
||||
|
||||
let color =
|
||||
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
|
||||
|
|
@ -114,11 +100,11 @@ fn color_editor(
|
|||
(
|
||||
color,
|
||||
Stack::rows(
|
||||
Label::new(label)
|
||||
.and(Slider::<f32>::new(hue, 0., 360.))
|
||||
.and(Input::new(hue_text))
|
||||
label
|
||||
.and(hue.slider_between(0., 360.))
|
||||
.and(hue_text.into_input())
|
||||
.and(Slider::<ZeroToOne>::from_value(saturation))
|
||||
.and(Input::new(saturation_text)),
|
||||
.and(saturation_text.into_input()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -128,69 +114,64 @@ fn fixed_themes(
|
|||
secondary: Dynamic<FixedTheme>,
|
||||
tertiary: Dynamic<FixedTheme>,
|
||||
) -> impl MakeWidget {
|
||||
Stack::rows(
|
||||
Label::new("Fixed")
|
||||
.and(fixed_theme(primary, "Primary"))
|
||||
.and(fixed_theme(secondary, "Secondary"))
|
||||
.and(fixed_theme(tertiary, "Tertiary")),
|
||||
)
|
||||
.contain()
|
||||
.expand()
|
||||
"Fixed"
|
||||
.and(fixed_theme(primary, "Primary"))
|
||||
.and(fixed_theme(secondary, "Secondary"))
|
||||
.and(fixed_theme(tertiary, "Tertiary"))
|
||||
.into_rows()
|
||||
.contain()
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
Stack::columns(
|
||||
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.dim_color),
|
||||
&format!("Dim {label}"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
on_color.clone(),
|
||||
&format!("On {label} Fixed"),
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.on_color_variant),
|
||||
&format!("Variant On {label} Fixed"),
|
||||
color,
|
||||
)),
|
||||
)
|
||||
.contain()
|
||||
.expand()
|
||||
|
||||
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.dim_color),
|
||||
&format!("Dim {label}"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
on_color.clone(),
|
||||
&format!("On {label} Fixed"),
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.on_color_variant),
|
||||
&format!("Variant On {label} Fixed"),
|
||||
color,
|
||||
))
|
||||
.into_columns()
|
||||
.contain()
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
|
||||
ModeSwitch::new(
|
||||
mode,
|
||||
Stack::rows(
|
||||
Label::new(match mode {
|
||||
ThemeMode::Light => "Light",
|
||||
ThemeMode::Dark => "Dark",
|
||||
})
|
||||
.and(
|
||||
Stack::columns(
|
||||
color_theme(theme.map_each(|theme| theme.primary), "Primary")
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.secondary),
|
||||
"Secondary",
|
||||
))
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.tertiary),
|
||||
"Tertiary",
|
||||
))
|
||||
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
|
||||
)
|
||||
.contain()
|
||||
.expand(),
|
||||
)
|
||||
.and(surface_theme(theme.map_each(|theme| theme.surface))),
|
||||
)
|
||||
.contain(),
|
||||
match mode {
|
||||
ThemeMode::Light => "Light",
|
||||
ThemeMode::Dark => "Dark",
|
||||
}
|
||||
.and(
|
||||
color_theme(theme.map_each(|theme| theme.primary), "Primary")
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.secondary),
|
||||
"Secondary",
|
||||
))
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.tertiary),
|
||||
"Tertiary",
|
||||
))
|
||||
.and(color_theme(theme.map_each(|theme| theme.error), "Error"))
|
||||
.into_columns()
|
||||
.contain()
|
||||
.expand(),
|
||||
)
|
||||
.and(surface_theme(theme.map_each(|theme| theme.surface)))
|
||||
.into_rows()
|
||||
.contain()
|
||||
.themed_mode(mode)
|
||||
.expand()
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +260,7 @@ fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
|
|||
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let dim_color = theme.map_each(|theme| theme.color_dim);
|
||||
let bright_color = theme.map_each(|theme| theme.color_bright);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
let container = theme.map_each(|theme| theme.container);
|
||||
let on_container = theme.map_each(|theme| theme.on_container);
|
||||
|
|
@ -289,6 +271,11 @@ fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
|||
&format!("{label} Dim"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
bright_color.clone(),
|
||||
&format!("{label} bright"),
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
on_color.clone(),
|
||||
&format!("On {label}"),
|
||||
|
|
@ -310,7 +297,7 @@ fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
|||
}
|
||||
|
||||
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
|
||||
Label::new(label)
|
||||
label
|
||||
.with(&TextColor, text)
|
||||
.with(&WidgetBackground, background)
|
||||
.fit_horizontally()
|
||||
|
|
|
|||
204
examples/tic-tac-toe.rs
Normal file
204
examples/tic-tac-toe.rs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
use std::fmt::Display;
|
||||
use std::iter;
|
||||
use std::ops::Not;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::button::ButtonKind;
|
||||
use gooey::{Run, WithClone};
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let app = Dynamic::default();
|
||||
app.map_each(app.with_clone(|app| {
|
||||
move |state: &AppState| match state {
|
||||
AppState::Playing => play_screen(&app).make_widget(),
|
||||
AppState::Winner(winner) => game_end(*winner, &app).make_widget(),
|
||||
}
|
||||
}))
|
||||
.switcher()
|
||||
.contain()
|
||||
.width(Lp::inches(2)..Lp::inches(6))
|
||||
.height(Lp::inches(2)..Lp::inches(6))
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Playing,
|
||||
Winner(Option<Player>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum Player {
|
||||
X,
|
||||
O,
|
||||
}
|
||||
|
||||
impl Display for Player {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Player::X => f.write_str("X"),
|
||||
Player::O => f.write_str("O"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for Player {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
Self::X => Self::O,
|
||||
Self::O => Self::X,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GameState {
|
||||
app: Dynamic<AppState>,
|
||||
current_player: Player,
|
||||
cells: Vec<Option<Player>>,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
fn new_game(app: &Dynamic<AppState>) -> Self {
|
||||
Self {
|
||||
app: app.clone(),
|
||||
// Bad RNG: if we have an even milliseconds in the current
|
||||
// timestamp, it's O's turn first.
|
||||
current_player: if SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("invalid system time")
|
||||
.as_millis()
|
||||
% 2
|
||||
== 0
|
||||
{
|
||||
Player::O
|
||||
} else {
|
||||
Player::X
|
||||
},
|
||||
cells: iter::repeat(None).take(9).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn play(&mut self, row: usize, column: usize) {
|
||||
let player = self.current_player;
|
||||
self.current_player = !player;
|
||||
|
||||
self.cells[row * 3 + column] = Some(player);
|
||||
|
||||
if let Some(winner) = self.check_for_winner() {
|
||||
self.app.set(AppState::Winner(Some(winner)));
|
||||
} else if self.cells.iter().all(Option::is_some) {
|
||||
self.app.set(AppState::Winner(None));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_winner(&self) -> Option<Player> {
|
||||
// Rows and columns
|
||||
for i in 0..3 {
|
||||
if let Some(winner) = self
|
||||
.winner_in_cells([[i, 0], [i, 1], [i, 2]])
|
||||
.or_else(|| self.winner_in_cells([[0, i], [1, i], [2, i]]))
|
||||
{
|
||||
return Some(winner);
|
||||
}
|
||||
}
|
||||
|
||||
// Diagonals
|
||||
self.winner_in_cells([[0, 0], [1, 1], [2, 2]])
|
||||
.or_else(|| self.winner_in_cells([[2, 0], [1, 1], [0, 2]]))
|
||||
}
|
||||
|
||||
fn winner_in_cells(&self, cells: [[usize; 2]; 3]) -> Option<Player> {
|
||||
match (
|
||||
self.cell(cells[0][0], cells[0][1]),
|
||||
self.cell(cells[1][0], cells[1][1]),
|
||||
self.cell(cells[2][0], cells[2][1]),
|
||||
) {
|
||||
(Some(a), Some(b), Some(c)) if a == b && b == c => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cell(&self, row: usize, column: usize) -> Option<Player> {
|
||||
self.cells[row * 3 + column]
|
||||
}
|
||||
}
|
||||
|
||||
fn game_end(winner: Option<Player>, app: &Dynamic<AppState>) -> impl MakeWidget {
|
||||
// TODO we need typography styles
|
||||
let app = app.clone();
|
||||
let label = if let Some(winner) = winner {
|
||||
format!("{winner:?} wins!")
|
||||
} else {
|
||||
String::from("No winner")
|
||||
};
|
||||
|
||||
label
|
||||
.and(
|
||||
"Play Again"
|
||||
.into_button()
|
||||
.on_click(move |_| {
|
||||
app.set(AppState::Playing);
|
||||
})
|
||||
.into_default(),
|
||||
)
|
||||
.into_rows()
|
||||
.centered()
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn play_screen(app: &Dynamic<AppState>) -> impl MakeWidget {
|
||||
let game = Dynamic::new(GameState::new_game(app));
|
||||
let current_player_label = game.map_each(|state| format!("{}'s Turn", state.current_player));
|
||||
|
||||
current_player_label.and(play_grid(&game)).into_rows()
|
||||
}
|
||||
|
||||
fn play_grid(game: &Dynamic<GameState>) -> impl MakeWidget {
|
||||
row_of_squares(0, game)
|
||||
.expand()
|
||||
.and(row_of_squares(1, game).expand())
|
||||
.and(row_of_squares(2, game).expand())
|
||||
.into_rows()
|
||||
}
|
||||
|
||||
fn row_of_squares(row: usize, game: &Dynamic<GameState>) -> impl MakeWidget {
|
||||
square(row, 0, game)
|
||||
.expand()
|
||||
.and(square(row, 1, game).expand())
|
||||
.and(square(row, 2, game).expand())
|
||||
.into_columns()
|
||||
}
|
||||
|
||||
fn square(row: usize, column: usize, game: &Dynamic<GameState>) -> impl MakeWidget {
|
||||
let game = game.clone();
|
||||
let enabled = Dynamic::new(true);
|
||||
let label = Dynamic::default();
|
||||
(&enabled, &label).with_clone(|(enabled, label)| {
|
||||
game.for_each(move |state| {
|
||||
let Some(player) = state.cell(row, column) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if enabled.update(false) {
|
||||
label.update(player.to_string());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
label
|
||||
.clone()
|
||||
.into_button()
|
||||
.kind(ButtonKind::Outline)
|
||||
.on_click(move |_| game.lock().play(row, column))
|
||||
.with_enabled(enabled)
|
||||
.pad()
|
||||
.expand()
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ proc-macro = true
|
|||
|
||||
[dependencies]
|
||||
attribute-derive = "0.8.1"
|
||||
manyhow = "0.9.0"
|
||||
manyhow = "0.10.0"
|
||||
proc-macro2 = "1.0.69"
|
||||
quote = "1.0.33"
|
||||
quote-use = "0.7.2"
|
||||
|
|
|
|||
|
|
@ -1,66 +1,114 @@
|
|||
use manyhow::bail;
|
||||
use manyhow::{bail, ensure};
|
||||
use quote::ToTokens;
|
||||
use syn::{Field, ItemStruct};
|
||||
use syn::{Data, DeriveInput, Field, Variant};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub fn linear_interpolate(
|
||||
ItemStruct {
|
||||
ident,
|
||||
DeriveInput {
|
||||
ident: item_ident,
|
||||
generics,
|
||||
fields,
|
||||
data,
|
||||
..
|
||||
}: ItemStruct,
|
||||
}: DeriveInput,
|
||||
) -> Result<TokenStream> {
|
||||
if let Some(generic) = generics.type_params().next() {
|
||||
bail!(generic, "generics not supported");
|
||||
}
|
||||
|
||||
let fields = match fields {
|
||||
syn::Fields::Unit => bail!(ident, "unit structs are not supported"),
|
||||
fields => fields
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, Field { ident, .. })| {
|
||||
let ident = ident
|
||||
.map(ToTokens::into_token_stream)
|
||||
.unwrap_or_else(|| proc_macro2::Literal::usize_unsuffixed(idx).into_token_stream());
|
||||
quote!(#ident: ::gooey::animation::LinearInterpolate::lerp(&self.#ident, &__target.#ident, __percent),)
|
||||
}),
|
||||
let doc;
|
||||
|
||||
let body = match data {
|
||||
Data::Struct(data) => {
|
||||
let fields = match data.fields {
|
||||
syn::Fields::Unit => bail!(item_ident, "unit structs are not supported"),
|
||||
fields => fields
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, Field { ident, .. })| {
|
||||
let ident = ident
|
||||
.map(ToTokens::into_token_stream)
|
||||
.unwrap_or_else(|| proc_macro2::Literal::usize_unsuffixed(idx).into_token_stream());
|
||||
quote!(#ident: ::gooey::animation::LinearInterpolate::lerp(&self.#ident, &__target.#ident, __percent),)
|
||||
}),
|
||||
};
|
||||
doc = "# Panics\n Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range).";
|
||||
quote!(#item_ident{#(#fields)*})
|
||||
}
|
||||
Data::Enum(data) => {
|
||||
let variants = data
|
||||
.variants
|
||||
.into_iter()
|
||||
.map(
|
||||
|Variant {
|
||||
ident,
|
||||
fields,
|
||||
discriminant,
|
||||
..
|
||||
}| {
|
||||
if let Some(discriminant) = discriminant {
|
||||
bail!(discriminant, "discriminants are not supported");
|
||||
}
|
||||
ensure!(fields.is_empty(), fields, "enum fields are not supported");
|
||||
Ok(quote!(#item_ident::#ident #fields))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let last = variants
|
||||
.last()
|
||||
.map(ToTokens::to_token_stream)
|
||||
.unwrap_or_else(|| quote!(unreachable!()));
|
||||
|
||||
let idx: Vec<_> = (0..variants.len()).collect();
|
||||
doc = "# Panics\n Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range).";
|
||||
quote! {
|
||||
# use ::gooey::animation::LinearInterpolate;
|
||||
fn variant_to_index(__v: &#item_ident) -> usize {
|
||||
match __v {
|
||||
#(#variants => #idx,)*
|
||||
}
|
||||
}
|
||||
let __self = variant_to_index(&self);
|
||||
let __target = variant_to_index(&__target);
|
||||
match LinearInterpolate::lerp(&__self, &__target, __percent) {
|
||||
#(#idx => #variants,)*
|
||||
_ => #last,
|
||||
}
|
||||
}
|
||||
}
|
||||
Data::Union(union) => bail!((union.union_token, union.fields), "unions not supported"),
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl ::gooey::animation::LinearInterpolate for #ident {
|
||||
impl ::gooey::animation::LinearInterpolate for #item_ident {
|
||||
#[doc = #doc]
|
||||
fn lerp(&self, __target: &Self, __percent: f32) -> Self {
|
||||
#ident{#(#fields)*}
|
||||
#body
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! expansion_snapshot {
|
||||
(#[derive($fn:expr)]$($tokens:tt)*) => {{
|
||||
use insta::assert_snapshot;
|
||||
use prettyplease::unparse;
|
||||
use syn::{parse2, parse_quote};
|
||||
let input = parse_quote!($($tokens)*);
|
||||
let output = $fn(input).unwrap();
|
||||
assert_snapshot!(unparse(&parse2(output).unwrap()))
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
expansion_snapshot! {
|
||||
mod test {
|
||||
use super::*;
|
||||
expansion_snapshot! {struct_
|
||||
#[derive(linear_interpolate)]
|
||||
struct HelloWorld {
|
||||
fielda: Hello,
|
||||
fieldb: World,
|
||||
}
|
||||
};
|
||||
expansion_snapshot! {
|
||||
}
|
||||
expansion_snapshot! {tuple_struct
|
||||
#[derive(linear_interpolate)]
|
||||
struct HelloWorld(Hello, World);
|
||||
};
|
||||
}
|
||||
expansion_snapshot! {enum_
|
||||
#[derive(linear_interpolate)]
|
||||
enum Enum{A, B}
|
||||
}
|
||||
expansion_snapshot! {empty_enum
|
||||
#[derive(linear_interpolate)]
|
||||
enum Enum{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,28 @@
|
|||
use manyhow::{manyhow, Result};
|
||||
use quote_use::quote_use as quote;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote_use::quote_use as quote;
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! expansion_snapshot {
|
||||
($name:ident $($tokens:tt)*) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
expansion_snapshot!{$($tokens)*}
|
||||
}
|
||||
};
|
||||
(#[derive($fn:expr)]$($tokens:tt)*) => {{
|
||||
use insta::assert_snapshot;
|
||||
use prettyplease::unparse;
|
||||
use syn::{parse2, parse_quote};
|
||||
let input = parse_quote!($($tokens)*);
|
||||
let output = $fn(input).unwrap();
|
||||
match &parse2(output.clone()) {
|
||||
Ok(ok) => assert_snapshot!(unparse(ok)),
|
||||
Err(_) => panic!("{output}"),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
mod animation;
|
||||
|
||||
#[manyhow(proc_macro_derive(LinearInterpolate))]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
source: gooey-macros/src/animation.rs
|
||||
expression: unparse(ok)
|
||||
---
|
||||
impl ::gooey::animation::LinearInterpolate for Enum {
|
||||
/**# Panics
|
||||
Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range).*/
|
||||
fn lerp(&self, __target: &Self, __percent: f32) -> Self {
|
||||
fn variant_to_index(__v: &Enum) -> usize {
|
||||
match __v {}
|
||||
}
|
||||
let __self = variant_to_index(&self);
|
||||
let __target = variant_to_index(&self);
|
||||
match ::gooey::animation::LinearInterpolate::lerp(
|
||||
&__self,
|
||||
&__target,
|
||||
__percent,
|
||||
) {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
source: gooey-macros/src/animation.rs
|
||||
expression: unparse(ok)
|
||||
---
|
||||
impl ::gooey::animation::LinearInterpolate for Enum {
|
||||
/**# Panics
|
||||
Panics if the the enum variants are overflown (this can only happen on percentages outside 0..1 range).*/
|
||||
fn lerp(&self, __target: &Self, __percent: f32) -> Self {
|
||||
fn variant_to_index(__v: &Enum) -> usize {
|
||||
match __v {
|
||||
Enum::A => 0usize,
|
||||
Enum::B => 1usize,
|
||||
}
|
||||
}
|
||||
let __self = variant_to_index(&self);
|
||||
let __target = variant_to_index(&self);
|
||||
match ::gooey::animation::LinearInterpolate::lerp(
|
||||
&__self,
|
||||
&__target,
|
||||
__percent,
|
||||
) {
|
||||
0usize => Enum::A,
|
||||
1usize => Enum::B,
|
||||
_ => Enum::B,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
---
|
||||
source: src/animation.rs
|
||||
expression: unparse(&parse2(output).unwrap())
|
||||
source: gooey-macros/src/animation.rs
|
||||
expression: unparse(ok)
|
||||
---
|
||||
impl ::gooey::animation::LinearInterpolate for HelloWorld {
|
||||
/**# Panics
|
||||
Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range).*/
|
||||
fn lerp(&self, __target: &Self, __percent: f32) -> Self {
|
||||
HelloWorld {
|
||||
fielda: ::gooey::animation::LinearInterpolate::lerp(
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
---
|
||||
source: gooey-macros/src/animation.rs
|
||||
expression: unparse(&parse2(output).unwrap())
|
||||
expression: unparse(ok)
|
||||
---
|
||||
impl ::gooey::animation::LinearInterpolate for HelloWorld {
|
||||
/**# Panics
|
||||
Panics if any field's lerp panics (this should only happen on percentages outside 0..1 range).*/
|
||||
fn lerp(&self, __target: &Self, __percent: f32) -> Self {
|
||||
HelloWorld {
|
||||
0: ::gooey::animation::LinearInterpolate::lerp(
|
||||
137
src/animation.rs
137
src/animation.rs
|
|
@ -39,22 +39,25 @@
|
|||
|
||||
pub mod easings;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::ops::{ControlFlow, Deref, Div, Mul};
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use alot::{LotId, Lots};
|
||||
use derive_more::From;
|
||||
use intentional::Cast;
|
||||
use kempt::Set;
|
||||
use kludgine::figures::Ranged;
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::easings::Linear;
|
||||
use crate::styles::Component;
|
||||
use crate::styles::{Component, RequireInvalidation};
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::Dynamic;
|
||||
|
||||
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
|
||||
|
|
@ -65,9 +68,7 @@ fn thread_state() -> MutexGuard<'static, Animating> {
|
|||
THREAD.get_or_init(|| {
|
||||
thread::spawn(animation_thread);
|
||||
});
|
||||
ANIMATIONS
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
ANIMATIONS.lock().ignore_poison()
|
||||
}
|
||||
|
||||
fn animation_thread() {
|
||||
|
|
@ -75,9 +76,7 @@ fn animation_thread() {
|
|||
loop {
|
||||
if state.running.is_empty() {
|
||||
state.last_updated = None;
|
||||
state = NEW_ANIMATIONS
|
||||
.wait(state)
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
state = NEW_ANIMATIONS.wait(state).ignore_poison();
|
||||
} else {
|
||||
let start = Instant::now();
|
||||
let last_tick = state.last_updated.unwrap_or(start);
|
||||
|
|
@ -610,12 +609,49 @@ impl Animate for Duration {
|
|||
}
|
||||
|
||||
/// Performs a linear interpolation between two values.
|
||||
///
|
||||
/// This trait can be derived for structs and fieldless enums.
|
||||
///
|
||||
/// Note: for fields that don't implement [`LinerarInterpolate`](trait@LinearInterpolate)
|
||||
/// the wrappers [`BinaryLerp`] and [`ImmediateLerp`] can be used.
|
||||
///
|
||||
/// ```
|
||||
/// use gooey::animation::{BinaryLerp, ImmediateLerp, LinearInterpolate};
|
||||
/// use gooey::kludgine::Color;
|
||||
///
|
||||
/// #[derive(LinearInterpolate, PartialEq, Debug)]
|
||||
/// struct Struct(Color, BinaryLerp<&'static str>, ImmediateLerp<&'static str>);
|
||||
///
|
||||
/// let from = Struct(Color::BLACK, "hello".into(), "hello".into());
|
||||
/// let to = Struct(Color::WHITE, "world".into(), "world".into());
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// from.lerp(&to, 0.41),
|
||||
/// Struct(Color::DIMGRAY, "hello".into(), "world".into())
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// from.lerp(&to, 0.663),
|
||||
/// Struct(Color::DARKGRAY, "world".into(), "world".into())
|
||||
/// );
|
||||
///
|
||||
/// #[derive(LinearInterpolate, PartialEq, Debug)]
|
||||
/// enum Enum {
|
||||
/// A,
|
||||
/// B,
|
||||
/// C,
|
||||
/// }
|
||||
/// assert_eq!(Enum::A.lerp(&Enum::B, 0.4), Enum::A);
|
||||
/// assert_eq!(Enum::A.lerp(&Enum::C, 0.1), Enum::A);
|
||||
/// assert_eq!(Enum::A.lerp(&Enum::C, 0.4), Enum::B);
|
||||
/// assert_eq!(Enum::A.lerp(&Enum::C, 0.9), Enum::C);
|
||||
/// ```
|
||||
pub trait LinearInterpolate: PartialEq {
|
||||
/// Interpolate linearly between `self` and `target` using `percent`.
|
||||
#[must_use]
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self;
|
||||
}
|
||||
|
||||
/// Derives [`LinerarInterpolate`](trait@LinearInterpolate) for structs and fieldless enums.
|
||||
pub use gooey_macros::LinearInterpolate;
|
||||
|
||||
macro_rules! impl_lerp_for_int {
|
||||
|
|
@ -641,9 +677,9 @@ macro_rules! impl_lerp_for_uint {
|
|||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
let percent = $float::from(percent);
|
||||
if let Some(delta) = target.checked_sub(*self) {
|
||||
*self + (delta as $float * percent).round() as $type
|
||||
self.saturating_add((delta as $float * percent).round() as $type)
|
||||
} else {
|
||||
*self - ((*self - *target) as $float * percent).round() as $type
|
||||
self.saturating_sub(((*self - *target) as $float * percent).round() as $type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -701,8 +737,10 @@ impl PercentBetween for bool {
|
|||
fn integer_lerps() {
|
||||
#[track_caller]
|
||||
fn test_lerps<T: LinearInterpolate + Debug + Eq>(a: &T, b: &T, mid: &T) {
|
||||
assert_eq!(&b.lerp(a, 1.), a);
|
||||
assert_eq!(&a.lerp(b, 1.), b);
|
||||
assert_eq!(&a.lerp(b, 0.), a);
|
||||
assert_eq!(&b.lerp(a, 0.), b);
|
||||
assert_eq!(&a.lerp(b, 0.5), mid);
|
||||
}
|
||||
|
||||
|
|
@ -733,7 +771,7 @@ impl LinearInterpolate for Color {
|
|||
///
|
||||
/// This wrapper can be used to add [`LinearInterpolate`] to types that normally
|
||||
/// don't support interpolation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, From)]
|
||||
pub struct BinaryLerp<T>(T);
|
||||
|
||||
impl<T> LinearInterpolate for BinaryLerp<T>
|
||||
|
|
@ -754,7 +792,7 @@ where
|
|||
///
|
||||
/// This wrapper can be used to add [`LinearInterpolate`] to types that normally
|
||||
/// don't support interpolation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, From)]
|
||||
pub struct ImmediateLerp<T>(T);
|
||||
|
||||
impl<T> LinearInterpolate for ImmediateLerp<T>
|
||||
|
|
@ -780,8 +818,14 @@ macro_rules! impl_percent_between {
|
|||
($type:ident, $float:ident) => {
|
||||
impl PercentBetween for $type {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
assert!(min <= max, "percent_between requires min <= max");
|
||||
assert!(
|
||||
self >= min && self <= max,
|
||||
"self must satisfy min <= self <= max"
|
||||
);
|
||||
|
||||
let range = *max - *min;
|
||||
ZeroToOne::from(*self as $float / range as $float)
|
||||
ZeroToOne::from((*self - *min) as $float / range as $float)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -809,17 +853,60 @@ impl PercentBetween for Color {
|
|||
min: Color,
|
||||
max: Color,
|
||||
func: impl Fn(Color) -> u8,
|
||||
) -> ZeroToOne {
|
||||
func(value).percent_between(&func(min), &func(max))
|
||||
) -> Option<ZeroToOne> {
|
||||
let value = func(value);
|
||||
let min = func(min);
|
||||
let max = func(max);
|
||||
match min.cmp(&max) {
|
||||
Ordering::Less => Some(value.percent_between(&min, &max)),
|
||||
Ordering::Equal => None,
|
||||
Ordering::Greater => Some(value.percent_between(&max, &min).one_minus()),
|
||||
}
|
||||
}
|
||||
|
||||
channel_percent(*self, *min, *max, Color::red)
|
||||
* channel_percent(*self, *min, *max, Color::green)
|
||||
* channel_percent(*self, *min, *max, Color::blue)
|
||||
* channel_percent(*self, *min, *max, Color::alpha)
|
||||
let mut total_percent_change = 0.;
|
||||
let mut different_channels = 0_u8;
|
||||
|
||||
for func in [Color::red, Color::green, Color::blue, Color::alpha] {
|
||||
if let Some(red) = channel_percent(*self, *min, *max, func) {
|
||||
total_percent_change += *red;
|
||||
different_channels += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if different_channels > 0 {
|
||||
ZeroToOne::new(total_percent_change / f32::from(different_channels))
|
||||
} else {
|
||||
ZeroToOne::ZERO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_percent_between() {
|
||||
assert_eq!(1_u8.percent_between(&1_u8, &2_u8), ZeroToOne::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_lerp() {
|
||||
let gray = Color::new(51, 51, 51, 51);
|
||||
let percent_gray = gray.percent_between(&Color::CLEAR_BLACK, &Color::WHITE);
|
||||
|
||||
assert_eq!(gray, Color::CLEAR_BLACK.lerp(&Color::WHITE, *percent_gray));
|
||||
|
||||
let gray = Color::new(51, 51, 51, 255);
|
||||
let percent_gray = gray.percent_between(&Color::BLACK, &Color::WHITE);
|
||||
|
||||
assert_eq!(gray, Color::BLACK.lerp(&Color::WHITE, *percent_gray));
|
||||
|
||||
let red_green = Color::RED.lerp(&Color::GREEN, 0.5);
|
||||
let percent_between = red_green.percent_between(&Color::RED, &Color::GREEN);
|
||||
// Why 1 / 255 / 4? This operation is working on u8s, and there are 4
|
||||
// channels that can be averaged. The percent is guaranteed to be within
|
||||
// this range, which works out to be 0.0098 percent.
|
||||
assert!((*percent_between - 0.5).abs() < 1. / 255. / 4.);
|
||||
}
|
||||
|
||||
/// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity.
|
||||
///
|
||||
/// Because of these restrictions, this type implements `Ord` and `Eq`.
|
||||
|
|
@ -918,19 +1005,19 @@ impl PartialEq<f32> for ZeroToOne {
|
|||
}
|
||||
|
||||
impl Ord for ZeroToOne {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.total_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ZeroToOne {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<f32> for ZeroToOne {
|
||||
fn partial_cmp(&self, other: &f32) -> Option<std::cmp::Ordering> {
|
||||
fn partial_cmp(&self, other: &f32) -> Option<Ordering> {
|
||||
Some(self.0.total_cmp(other))
|
||||
}
|
||||
}
|
||||
|
|
@ -1012,6 +1099,12 @@ impl TryFrom<Component> for EasingFunction {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for EasingFunction {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs easing for value interpolation.
|
||||
pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static {
|
||||
/// Eases a value ranging between zero and one. The resulting value does not
|
||||
|
|
|
|||
324
src/context.rs
324
src/context.rs
|
|
@ -1,9 +1,11 @@
|
|||
//! Types that provide access to the Gooey runtime.
|
||||
use std::borrow::Cow;
|
||||
use std::hash::Hash;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use kempt::Set;
|
||||
use kludgine::app::winit::event::{
|
||||
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
|
|
@ -13,12 +15,13 @@ use kludgine::shapes::{Shape, StrokeOptions};
|
|||
use kludgine::{Color, Kludgine};
|
||||
|
||||
use crate::graphics::Graphics;
|
||||
use crate::styles::components::{HighlightColor, WidgetBackground};
|
||||
use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair, VisualOrder};
|
||||
use crate::styles::components::{HighlightColor, LayoutOrder, WidgetBackground};
|
||||
use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::window::{RunningWindow, ThemeMode};
|
||||
use crate::window::{CursorState, RunningWindow, ThemeMode};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A context to an event function.
|
||||
|
|
@ -182,7 +185,10 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
|
||||
let mut activation_changes = 0;
|
||||
while activation_changes < MAX_ITERS {
|
||||
let active = self.pending_state.active.clone();
|
||||
let active = self
|
||||
.pending_state
|
||||
.active
|
||||
.and_then(|w| self.current_node.tree.widget(w));
|
||||
if self.current_node.tree.active_widget() == active.as_ref().map(|w| w.node_id) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -199,13 +205,16 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
Err(()) => false,
|
||||
};
|
||||
if new {
|
||||
if let Some(active) = self.pending_state.active.clone() {
|
||||
let active = self
|
||||
.pending_state
|
||||
.active
|
||||
.and_then(|w| self.current_node.tree.widget(w));
|
||||
if let Some(active) = &active {
|
||||
active
|
||||
.lock()
|
||||
.as_widget()
|
||||
.activate(&mut self.for_other(&active));
|
||||
.activate(&mut self.for_other(active));
|
||||
}
|
||||
self.pending_state.active = active;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -219,30 +228,36 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
|
||||
let mut focus_changes = 0;
|
||||
while focus_changes < MAX_ITERS {
|
||||
let focus = self.pending_state.focus.clone();
|
||||
let focus = match self
|
||||
.pending_state
|
||||
.focus
|
||||
.and_then(|w| self.current_node.tree.widget(w))
|
||||
{
|
||||
Some(focus) => self.for_other(&focus).enabled().then_some(focus),
|
||||
None => None,
|
||||
};
|
||||
if self.current_node.tree.focused_widget() == focus.as_ref().map(|w| w.node_id) {
|
||||
break;
|
||||
}
|
||||
focus_changes += 1;
|
||||
|
||||
self.pending_state.focus = focus.and_then(|mut focus| loop {
|
||||
if focus
|
||||
.lock()
|
||||
.as_widget()
|
||||
.accept_focus(&mut self.for_other(&focus))
|
||||
let mut focus_context = self.for_other(&focus);
|
||||
let accept_focus = focus_context.enabled()
|
||||
&& focus.lock().as_widget().accept_focus(&mut focus_context);
|
||||
drop(focus_context);
|
||||
|
||||
if accept_focus {
|
||||
break Some(focus.id());
|
||||
} else if let Some(next_focus) =
|
||||
focus.explicit_focus_target(self.pending_state.focus_is_advancing)
|
||||
{
|
||||
break Some(focus);
|
||||
} else if let Some(next_focus) = focus.next_focus() {
|
||||
focus = next_focus;
|
||||
} else {
|
||||
break self.next_focus_after(focus, VisualOrder::left_to_right());
|
||||
break self.next_focus_after(focus, self.pending_state.focus_is_advancing);
|
||||
}
|
||||
});
|
||||
let new = match self
|
||||
.current_node
|
||||
.tree
|
||||
.focus(self.pending_state.focus.as_ref())
|
||||
{
|
||||
let new = match self.current_node.tree.focus(self.pending_state.focus) {
|
||||
Ok(old) => {
|
||||
if let Some(old) = old {
|
||||
let mut old_context = self.for_other(&old);
|
||||
|
|
@ -253,7 +268,11 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
Err(()) => false,
|
||||
};
|
||||
if new {
|
||||
if let Some(focus) = self.pending_state.focus.clone() {
|
||||
if let Some(focus) = self
|
||||
.pending_state
|
||||
.focus
|
||||
.and_then(|w| self.current_node.tree.widget(w))
|
||||
{
|
||||
focus.lock().as_widget().focus(&mut self.for_other(&focus));
|
||||
}
|
||||
} else {
|
||||
|
|
@ -264,22 +283,49 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
if focus_changes == MAX_ITERS {
|
||||
tracing::error!("focus change force stopped after {focus_changes} sequential changes");
|
||||
}
|
||||
|
||||
// Check that our hover widget still exists. If not, we should try to find a new one.
|
||||
if let Some(hover) = self.current_node.tree.hovered_widget() {
|
||||
if self.current_node.tree.widget_from_node(hover).is_none() {
|
||||
self.update_hovered_widget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_focus_after(
|
||||
&mut self,
|
||||
mut focus: ManagedWidget,
|
||||
order: VisualOrder,
|
||||
) -> Option<ManagedWidget> {
|
||||
pub(crate) fn update_hovered_widget(&mut self) {
|
||||
self.cursor.widget = None;
|
||||
if let Some(location) = self.cursor.location {
|
||||
for widget in self.current_node.tree.widgets_under_point(location) {
|
||||
let mut widget_context = self.for_other(&widget);
|
||||
let Some(widget_layout) = widget_context.last_layout() else {
|
||||
continue;
|
||||
};
|
||||
let relative = location - widget_layout.origin;
|
||||
|
||||
if widget_context.hit_test(relative) {
|
||||
widget_context.hover(relative);
|
||||
drop(widget_context);
|
||||
self.cursor.widget = Some(widget.id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.cursor.widget.is_none() {
|
||||
self.clear_hover();
|
||||
}
|
||||
}
|
||||
|
||||
fn next_focus_after(&mut self, mut focus: ManagedWidget, advance: bool) -> Option<WidgetId> {
|
||||
// First, look within the current focus for any focusable children.
|
||||
let stop_at = focus.id();
|
||||
if let Some(focus) = self.next_focus_within(&focus, None, stop_at, order) {
|
||||
if let Some(focus) = self.next_focus_within(&focus, None, stop_at, advance) {
|
||||
return Some(focus);
|
||||
}
|
||||
|
||||
// Now, look for the next widget in each hierarchy
|
||||
let root = loop {
|
||||
if let Some(focus) = self.next_focus_sibling(&focus, stop_at, order) {
|
||||
if let Some(focus) = self.next_focus_sibling(&focus, stop_at, advance) {
|
||||
return Some(focus);
|
||||
}
|
||||
let Some(parent) = focus.parent() else {
|
||||
|
|
@ -290,16 +336,16 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
|
||||
// We've exhausted a forward scan, we can now start searching the final
|
||||
// parent, which is the root.
|
||||
self.next_focus_within(&root, None, stop_at, order)
|
||||
self.next_focus_within(&root, None, stop_at, advance)
|
||||
}
|
||||
|
||||
fn next_focus_sibling(
|
||||
&mut self,
|
||||
focus: &ManagedWidget,
|
||||
stop_at: WidgetId,
|
||||
order: VisualOrder,
|
||||
) -> Option<ManagedWidget> {
|
||||
self.next_focus_within(&focus.parent()?, Some(focus.id()), stop_at, order)
|
||||
advance: bool,
|
||||
) -> Option<WidgetId> {
|
||||
self.next_focus_within(&focus.parent()?, Some(focus.id()), stop_at, advance)
|
||||
}
|
||||
|
||||
/// Searches for the next focus inside of `focus`, returning `None` if
|
||||
|
|
@ -310,10 +356,14 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
focus: &ManagedWidget,
|
||||
start_at: Option<WidgetId>,
|
||||
stop_at: WidgetId,
|
||||
order: VisualOrder,
|
||||
) -> Option<ManagedWidget> {
|
||||
advance: bool,
|
||||
) -> Option<WidgetId> {
|
||||
let mut visual_order = self.get(&LayoutOrder);
|
||||
if !advance {
|
||||
visual_order = visual_order.rev();
|
||||
}
|
||||
let mut children = focus
|
||||
.visually_ordered_children(order)
|
||||
.visually_ordered_children(visual_order)
|
||||
.into_iter()
|
||||
.peekable();
|
||||
if let Some(start_at) = start_at {
|
||||
|
|
@ -331,13 +381,15 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
break;
|
||||
}
|
||||
|
||||
if child
|
||||
.lock()
|
||||
.as_widget()
|
||||
.accept_focus(&mut self.for_other(&child))
|
||||
{
|
||||
return Some(child);
|
||||
} else if let Some(focus) = self.next_focus_within(&child, None, stop_at, order) {
|
||||
let mut child_context = self.for_other(&child);
|
||||
let accept_focus = child_context.enabled()
|
||||
&& child.lock().as_widget().accept_focus(&mut child_context);
|
||||
drop(child_context);
|
||||
if accept_focus {
|
||||
return Some(child.id());
|
||||
} else if let Some(next_focus) = self.widget().explicit_focus_target(advance) {
|
||||
return Some(next_focus.id());
|
||||
} else if let Some(focus) = self.next_focus_within(&child, None, stop_at, advance) {
|
||||
return Some(focus);
|
||||
}
|
||||
}
|
||||
|
|
@ -345,14 +397,31 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Advances the focus from this widget to the next widget in `direction`.
|
||||
/// Advances the focus to the next widget after this widget in the
|
||||
/// configured focus order.
|
||||
///
|
||||
/// This widget does not need to be focused.
|
||||
pub fn advance_focus(&mut self, direction: VisualOrder) {
|
||||
// TODO check to see if the current node has an explicit next_focus (or
|
||||
// if we're going in the opposite direction, previous_focus).
|
||||
/// To focus in the reverse order, use [`EventContext::return_focus()`].
|
||||
pub fn advance_focus(&mut self) {
|
||||
self.move_focus(true);
|
||||
}
|
||||
|
||||
self.pending_state.focus = self.next_focus_after(self.current_node.clone(), direction);
|
||||
/// Returns the focus to the previous widget before this widget in the
|
||||
/// configured fous order.
|
||||
///
|
||||
/// To focus in the forward order, use [`EventContext::advance_focus()`].
|
||||
pub fn return_focus(&mut self) {
|
||||
self.move_focus(false);
|
||||
}
|
||||
|
||||
fn move_focus(&mut self, advance: bool) {
|
||||
if let Some(explicit_next_focus) = self.current_node.explicit_focus_target(advance) {
|
||||
self.for_other(&explicit_next_focus).focus();
|
||||
} else {
|
||||
self.pending_state.focus = self.next_focus_after(self.current_node.clone(), advance);
|
||||
}
|
||||
// It is important to set focus-is_advancing after `focus()` because it
|
||||
// sets it to `true` explicitly.
|
||||
self.pending_state.focus_is_advancing = advance;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,7 +522,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
/// Strokes an outline around this widget's contents.
|
||||
pub fn stroke_outline<Unit>(&mut self, color: Color, options: StrokeOptions<Unit>)
|
||||
where
|
||||
Unit: ScreenScale<Px = Px, Lp = Lp>,
|
||||
Unit: ScreenScale<Px = Px, Lp = Lp, UPx = UPx>,
|
||||
{
|
||||
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
|
||||
let focus_ring =
|
||||
|
|
@ -572,14 +641,23 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl
|
|||
/// context's widget and returns the result.
|
||||
pub fn layout(&mut self, available_space: Size<ConstraintLimit>) -> Size<UPx> {
|
||||
if self.persist_layout {
|
||||
self.graphics.current_node.reset_child_layouts();
|
||||
if let Some(cached) = self.graphics.current_node.begin_layout(available_space) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
self.graphics
|
||||
let result = self
|
||||
.graphics
|
||||
.current_node
|
||||
.clone()
|
||||
.lock()
|
||||
.as_widget()
|
||||
.layout(available_space, self)
|
||||
.layout(available_space, self);
|
||||
if self.persist_layout {
|
||||
self.graphics
|
||||
.current_node
|
||||
.persist_layout(available_space, result);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Sets the layout for `child` to `layout`.
|
||||
|
|
@ -664,38 +742,49 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, '
|
|||
/// specific widget.
|
||||
pub struct WidgetContext<'context, 'window> {
|
||||
current_node: ManagedWidget,
|
||||
redraw_status: &'context RedrawStatus,
|
||||
redraw_status: &'context InvalidationStatus,
|
||||
window: &'context mut RunningWindow<'window>,
|
||||
theme: Cow<'context, ThemePair>,
|
||||
cursor: &'context mut CursorState,
|
||||
pending_state: PendingState<'context>,
|
||||
theme_mode: ThemeMode,
|
||||
effective_styles: Styles,
|
||||
cache: WidgetCacheKey,
|
||||
}
|
||||
|
||||
impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||
pub(crate) fn new(
|
||||
current_node: ManagedWidget,
|
||||
redraw_status: &'context RedrawStatus,
|
||||
redraw_status: &'context InvalidationStatus,
|
||||
theme: &'context ThemePair,
|
||||
window: &'context mut RunningWindow<'window>,
|
||||
theme_mode: ThemeMode,
|
||||
cursor: &'context mut CursorState,
|
||||
) -> Self {
|
||||
let enabled = current_node.enabled(&WindowHandle {
|
||||
kludgine: window.handle(),
|
||||
redraw_status: redraw_status.clone(),
|
||||
});
|
||||
Self {
|
||||
pending_state: PendingState::Owned(PendingWidgetState {
|
||||
focus: current_node
|
||||
.tree
|
||||
.focused_widget()
|
||||
.and_then(|id| current_node.tree.widget_from_node(id)),
|
||||
.and_then(|id| current_node.tree.widget_from_node(id).map(|w| w.id())),
|
||||
active: current_node
|
||||
.tree
|
||||
.active_widget()
|
||||
.and_then(|id| current_node.tree.widget_from_node(id)),
|
||||
.and_then(|id| current_node.tree.widget_from_node(id).map(|w| w.id())),
|
||||
focus_is_advancing: false,
|
||||
}),
|
||||
effective_styles: current_node.effective_styles(),
|
||||
cache: WidgetCacheKey {
|
||||
theme_mode,
|
||||
enabled,
|
||||
},
|
||||
cursor,
|
||||
current_node,
|
||||
redraw_status,
|
||||
theme: Cow::Borrowed(theme),
|
||||
theme_mode,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
|
@ -708,8 +797,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
window: &mut *self.window,
|
||||
theme: Cow::Borrowed(self.theme.as_ref()),
|
||||
pending_state: self.pending_state.borrowed(),
|
||||
theme_mode: self.theme_mode,
|
||||
cache: self.cache,
|
||||
effective_styles: self.effective_styles.clone(),
|
||||
cursor: &mut *self.cursor,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -732,20 +822,30 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
let theme_mode = if let Some(mode) = theme_mode {
|
||||
mode.get_tracked(self)
|
||||
} else {
|
||||
self.theme_mode
|
||||
self.cache.theme_mode
|
||||
};
|
||||
WidgetContext {
|
||||
effective_styles,
|
||||
cache: WidgetCacheKey {
|
||||
theme_mode,
|
||||
enabled: current_node.enabled(&self.handle()),
|
||||
},
|
||||
current_node,
|
||||
redraw_status: self.redraw_status,
|
||||
window: &mut *self.window,
|
||||
theme,
|
||||
pending_state: self.pending_state.borrowed(),
|
||||
theme_mode,
|
||||
cursor: &mut *self.cursor,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if this widget is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
self.cache.enabled
|
||||
}
|
||||
|
||||
pub(crate) fn parent(&self) -> Option<ManagedWidget> {
|
||||
self.current_node.parent()
|
||||
}
|
||||
|
|
@ -755,6 +855,11 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
value.redraw_when_changed(self.handle());
|
||||
}
|
||||
|
||||
/// Ensures that this widget will be redrawn when `value` has been updated.
|
||||
pub fn invalidate_when_changed<T>(&self, value: &Dynamic<T>) {
|
||||
value.invalidate_when_changed(self.handle(), self.current_node.id());
|
||||
}
|
||||
|
||||
/// Returns the last layout of this widget.
|
||||
#[must_use]
|
||||
pub fn last_layout(&self) -> Option<Rect<Px>> {
|
||||
|
|
@ -766,7 +871,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Widget events relating to focus changes are deferred until after the all
|
||||
/// contexts for the currently firing event are dropped.
|
||||
pub fn focus(&mut self) {
|
||||
self.pending_state.focus = Some(self.current_node.clone());
|
||||
self.pending_state.focus_is_advancing = true;
|
||||
self.pending_state.focus = Some(self.current_node.id());
|
||||
}
|
||||
|
||||
pub(crate) fn clear_focus(&mut self) {
|
||||
|
|
@ -796,16 +902,11 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Widget events relating to activation changes are deferred until after
|
||||
/// the all contexts for the currently firing event are dropped.
|
||||
pub fn activate(&mut self) -> bool {
|
||||
if self
|
||||
.pending_state
|
||||
.active
|
||||
.as_ref()
|
||||
.map_or(true, |active| active != &self.current_node)
|
||||
{
|
||||
self.pending_state.active = Some(self.current_node.clone());
|
||||
true
|
||||
} else {
|
||||
if self.pending_state.active == Some(self.current_node.id()) {
|
||||
false
|
||||
} else {
|
||||
self.pending_state.active = Some(self.current_node.id());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -832,7 +933,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Returns true if this widget is currently the active widget.
|
||||
#[must_use]
|
||||
pub fn active(&self) -> bool {
|
||||
self.pending_state.active.as_ref() == Some(&self.current_node)
|
||||
self.pending_state.active == Some(self.current_node.id())
|
||||
}
|
||||
|
||||
/// Returns true if this widget is currently hovered, even if the cursor is
|
||||
|
|
@ -851,7 +952,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Returns true if this widget is currently focused for user input.
|
||||
#[must_use]
|
||||
pub fn focused(&self) -> bool {
|
||||
self.pending_state.focus.as_ref() == Some(&self.current_node)
|
||||
self.pending_state.focus == Some(self.current_node.id())
|
||||
}
|
||||
|
||||
/// Returns true if this widget is the target to activate when the user
|
||||
|
|
@ -945,7 +1046,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Returns the current theme in either light or dark mode.
|
||||
#[must_use]
|
||||
pub fn theme(&self) -> &Theme {
|
||||
match self.theme_mode {
|
||||
match self.cache.theme_mode {
|
||||
ThemeMode::Light => &self.theme.light,
|
||||
ThemeMode::Dark => &self.theme.dark,
|
||||
}
|
||||
|
|
@ -954,16 +1055,41 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Returns the opposite theme of [`Self::theme()`].
|
||||
#[must_use]
|
||||
pub fn inverse_theme(&self) -> &Theme {
|
||||
match self.theme_mode {
|
||||
match self.cache.theme_mode {
|
||||
ThemeMode::Light => &self.theme.dark,
|
||||
ThemeMode::Dark => &self.theme.light,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a key that can be checked to see if a widget should invalidate
|
||||
/// caches it stores.
|
||||
#[must_use]
|
||||
pub fn cache_key(&self) -> WidgetCacheKey {
|
||||
self.cache
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WindowHandle {
|
||||
kludgine: kludgine::app::WindowHandle<WindowCommand>,
|
||||
redraw_status: RedrawStatus,
|
||||
redraw_status: InvalidationStatus,
|
||||
}
|
||||
|
||||
impl Eq for WindowHandle {}
|
||||
|
||||
impl PartialEq for WindowHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(
|
||||
&self.redraw_status.invalidated,
|
||||
&other.redraw_status.invalidated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for WindowHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
Arc::as_ptr(&self.redraw_status.invalidated).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
|
|
@ -972,6 +1098,12 @@ impl WindowHandle {
|
|||
let _result = self.kludgine.send(WindowCommand::Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, widget: WidgetId) {
|
||||
if self.redraw_status.invalidate(widget) {
|
||||
self.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn AsEventContext<'_> {}
|
||||
|
|
@ -1005,8 +1137,9 @@ enum PendingState<'a> {
|
|||
|
||||
#[derive(Default)]
|
||||
struct PendingWidgetState {
|
||||
focus: Option<ManagedWidget>,
|
||||
active: Option<ManagedWidget>,
|
||||
focus_is_advancing: bool,
|
||||
focus: Option<WidgetId>,
|
||||
active: Option<WidgetId>,
|
||||
}
|
||||
|
||||
impl PendingState<'_> {
|
||||
|
|
@ -1036,11 +1169,12 @@ impl DerefMut for PendingState<'_> {
|
|||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct RedrawStatus {
|
||||
pub(crate) struct InvalidationStatus {
|
||||
refresh_sent: Arc<AtomicBool>,
|
||||
invalidated: Arc<Mutex<Set<WidgetId>>>,
|
||||
}
|
||||
|
||||
impl RedrawStatus {
|
||||
impl InvalidationStatus {
|
||||
pub fn should_send_refresh(&self) -> bool {
|
||||
self.refresh_sent
|
||||
.compare_exchange(false, true, Ordering::Release, Ordering::Acquire)
|
||||
|
|
@ -1050,6 +1184,15 @@ impl RedrawStatus {
|
|||
pub fn refresh_received(&self) {
|
||||
self.refresh_sent.store(false, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, widget: WidgetId) -> bool {
|
||||
let mut invalidated = self.invalidated.lock().ignore_poison();
|
||||
invalidated.insert(widget)
|
||||
}
|
||||
|
||||
pub fn invalidations(&self) -> MutexGuard<'_, Set<WidgetId>> {
|
||||
self.invalidated.lock().ignore_poison()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type chat can convert to a [`ManagedWidget`] through a [`WidgetContext`].
|
||||
|
|
@ -1113,3 +1256,22 @@ impl<T> MapManagedWidget<T> for ManagedWidget {
|
|||
map(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An type that contains information about the state of a widget.
|
||||
///
|
||||
/// This value can be stored and compared in future widget events. If the cache
|
||||
/// keys are not equal, the widget should clear all caches.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct WidgetCacheKey {
|
||||
theme_mode: ThemeMode,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for WidgetCacheKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme_mode: ThemeMode::default().inverse(),
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use std::ops::Sub;
|
|||
pub use kludgine;
|
||||
use kludgine::app::winit::error::EventLoopError;
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoUnsigned, ScreenUnit};
|
||||
use kludgine::figures::{Fraction, ScreenUnit};
|
||||
pub use names::Name;
|
||||
pub use utils::{Lazy, WithClone};
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ impl ConstraintLimit {
|
|||
where
|
||||
Unit: ScreenUnit,
|
||||
{
|
||||
let measured = measured.into_px(scale).into_unsigned();
|
||||
let measured = measured.into_upx(scale);
|
||||
match self {
|
||||
ConstraintLimit::Known(size) => size.max(measured),
|
||||
ConstraintLimit::ClippedAfter(_) => measured,
|
||||
|
|
|
|||
261
src/styles.rs
261
src/styles.rs
|
|
@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||
|
||||
use ahash::AHashMap;
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{Fraction, IntoUnsigned, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
|
||||
use kludgine::Color;
|
||||
use palette::{IntoColor, Okhsl, OklabHue, Srgb};
|
||||
|
||||
|
|
@ -86,8 +86,17 @@ impl Styles {
|
|||
self.0
|
||||
.get(&name)
|
||||
.and_then(|component| {
|
||||
component.redraw_when_changed(context);
|
||||
<Named::ComponentType>::try_from_component(component.get()).ok()
|
||||
match <Named::ComponentType>::try_from_component(component.get()) {
|
||||
Ok(value) => {
|
||||
if value.requires_invalidation() {
|
||||
component.invalidate_when_changed(context);
|
||||
} else {
|
||||
component.redraw_when_changed(context);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| component.default_value(context))
|
||||
}
|
||||
|
|
@ -219,6 +228,12 @@ impl TryFrom<Component> for Color {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for Color {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dimension> for Component {
|
||||
fn from(value: Dimension) -> Self {
|
||||
Self::Dimension(value)
|
||||
|
|
@ -236,6 +251,12 @@ impl TryFrom<Component> for Dimension {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for Dimension {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Px> for Component {
|
||||
fn from(value: Px) -> Self {
|
||||
Self::from(Dimension::from(value))
|
||||
|
|
@ -253,6 +274,12 @@ impl TryFrom<Component> for Px {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for Px {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Lp> for Component {
|
||||
fn from(value: Lp) -> Self {
|
||||
Self::from(Dimension::from(value))
|
||||
|
|
@ -270,6 +297,12 @@ impl TryFrom<Component> for Lp {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for Lp {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A 1-dimensional measurement that may be automatically calculated.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FlexibleDimension {
|
||||
|
|
@ -343,6 +376,7 @@ impl From<Lp> for Dimension {
|
|||
impl ScreenScale for Dimension {
|
||||
type Lp = Lp;
|
||||
type Px = Px;
|
||||
type UPx = UPx;
|
||||
|
||||
fn into_px(self, scale: kludgine::figures::Fraction) -> Px {
|
||||
match self {
|
||||
|
|
@ -365,6 +399,17 @@ impl ScreenScale for Dimension {
|
|||
fn from_lp(lp: Lp, _scale: kludgine::figures::Fraction) -> Self {
|
||||
Self::from(lp)
|
||||
}
|
||||
|
||||
fn into_upx(self, scale: Fraction) -> Self::UPx {
|
||||
match self {
|
||||
Dimension::Px(px) => px.into_unsigned(),
|
||||
Dimension::Lp(lp) => lp.into_upx(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_upx(px: Self::UPx, _scale: Fraction) -> Self {
|
||||
Self::from(px.into_signed())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<i32> for Dimension {
|
||||
|
|
@ -436,10 +481,10 @@ impl DimensionRange {
|
|||
#[must_use]
|
||||
pub fn clamp(&self, mut size: UPx, scale: Fraction) -> UPx {
|
||||
if let Some(min) = self.minimum() {
|
||||
size = size.max(min.into_px(scale).into_unsigned());
|
||||
size = size.max(min.into_upx(scale));
|
||||
}
|
||||
if let Some(max) = self.maximum() {
|
||||
size = size.min(max.into_px(scale).into_unsigned());
|
||||
size = size.min(max.into_upx(scale));
|
||||
}
|
||||
size
|
||||
}
|
||||
|
|
@ -563,6 +608,12 @@ impl TryFrom<Component> for DimensionRange {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for DimensionRange {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom component value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomComponent(Arc<dyn AnyComponent>);
|
||||
|
|
@ -571,7 +622,7 @@ impl CustomComponent {
|
|||
/// Wraps an arbitrary value so that it can be used as a [`Component`].
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
|
||||
T: RequireInvalidation + RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
|
||||
{
|
||||
Self(Arc::new(value))
|
||||
}
|
||||
|
|
@ -587,6 +638,12 @@ impl CustomComponent {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for CustomComponent {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
self.0.requires_invalidation()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentType for CustomComponent {
|
||||
fn into_component(self) -> Component {
|
||||
Component::Custom(self)
|
||||
|
|
@ -600,13 +657,13 @@ impl ComponentType for CustomComponent {
|
|||
}
|
||||
}
|
||||
|
||||
trait AnyComponent: Send + Sync + RefUnwindSafe + UnwindSafe + Debug {
|
||||
trait AnyComponent: RequireInvalidation + Send + Sync + RefUnwindSafe + UnwindSafe + Debug {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl<T> AnyComponent for T
|
||||
where
|
||||
T: RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
|
||||
T: RequireInvalidation + RefUnwindSafe + UnwindSafe + Debug + Send + Sync + 'static,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
|
@ -654,8 +711,20 @@ pub trait ComponentDefinition: NamedComponent {
|
|||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType;
|
||||
}
|
||||
|
||||
/// Describes whether a type should invalidate a widget.
|
||||
pub trait RequireInvalidation {
|
||||
/// Gooey tracks two different states:
|
||||
///
|
||||
/// - Whether to repaint the window
|
||||
/// - Whether to relayout a widget
|
||||
///
|
||||
/// If a value change of `self` may require a relayout, this should return
|
||||
/// true.
|
||||
fn requires_invalidation(&self) -> bool;
|
||||
}
|
||||
|
||||
/// A type that can be converted to and from [`Component`].
|
||||
pub trait ComponentType: Sized {
|
||||
pub trait ComponentType: RequireInvalidation + Sized {
|
||||
/// Returns this type, wrapped in a [`Component`].
|
||||
fn into_component(self) -> Component;
|
||||
/// Attempts to extract this type from `component`. If `component` does not
|
||||
|
|
@ -665,7 +734,7 @@ pub trait ComponentType: Sized {
|
|||
|
||||
impl<T> ComponentType for T
|
||||
where
|
||||
T: Into<Component> + TryFrom<Component, Error = Component>,
|
||||
T: RequireInvalidation + Into<Component> + TryFrom<Component, Error = Component>,
|
||||
{
|
||||
fn into_component(self) -> Component {
|
||||
self.into()
|
||||
|
|
@ -1095,6 +1164,8 @@ pub struct ColorTheme {
|
|||
pub color: Color,
|
||||
/// The primary color, dimmed for de-emphasized or disabled content.
|
||||
pub color_dim: Color,
|
||||
/// The primary color, brightened for highlighting content.
|
||||
pub color_bright: Color,
|
||||
/// The color for content that sits atop the primary color.
|
||||
pub on_color: Color,
|
||||
/// The backgrond color for containers.
|
||||
|
|
@ -1110,6 +1181,7 @@ impl ColorTheme {
|
|||
Self {
|
||||
color: source.color(40),
|
||||
color_dim: source.color(30),
|
||||
color_bright: source.color(45),
|
||||
on_color: source.color(100),
|
||||
container: source.color(90),
|
||||
on_container: source.color(10),
|
||||
|
|
@ -1122,6 +1194,7 @@ impl ColorTheme {
|
|||
Self {
|
||||
color: source.color(70),
|
||||
color_dim: source.color(60),
|
||||
color_bright: source.color(75),
|
||||
on_color: source.color(10),
|
||||
container: source.color(30),
|
||||
on_container: source.color(90),
|
||||
|
|
@ -1324,12 +1397,19 @@ impl ColorExt for Color {
|
|||
let (other_source, other_lightness) = self.into_source_and_lightness();
|
||||
let lightness_delta = other_lightness.difference_between(check_lightness);
|
||||
|
||||
let average_lightness = ZeroToOne::new((*check_lightness + *other_lightness) / 2.);
|
||||
|
||||
let source_change = check_source.contrast_between(other_source);
|
||||
|
||||
let other_alpha = ZeroToOne::new(self.alpha_f32());
|
||||
let alpha_delta = check_alpha.difference_between(other_alpha);
|
||||
|
||||
ZeroToOne::new((*lightness_delta + *source_change + *alpha_delta) / 3.)
|
||||
ZeroToOne::new(
|
||||
(*lightness_delta
|
||||
+ *average_lightness * *source_change
|
||||
+ *average_lightness * *alpha_delta)
|
||||
/ 3.,
|
||||
)
|
||||
}
|
||||
|
||||
fn most_contrasting(self, others: &[Self]) -> Self
|
||||
|
|
@ -1411,6 +1491,12 @@ impl TryFrom<Component> for VisualOrder {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for VisualOrder {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A horizontal direction.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum HorizontalOrder {
|
||||
|
|
@ -1514,6 +1600,12 @@ impl TryFrom<Component> for FocusableWidgets {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for FocusableWidgets {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A description of the level of depth a
|
||||
/// [`Container`](crate::widgets::Container) is nested at.
|
||||
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
|
|
@ -1563,6 +1655,12 @@ impl TryFrom<Component> for ContainerLevel {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for ContainerLevel {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder of [`ColorScheme`]s.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ColorSchemeBuilder {
|
||||
|
|
@ -1583,27 +1681,21 @@ pub struct ColorSchemeBuilder {
|
|||
/// The neutral variant color of the scheme. If not provided, a mostly
|
||||
/// desaturated variation of the primary color will be used.
|
||||
pub neutral_variant: Option<ColorSource>,
|
||||
hue_shift: f32,
|
||||
hue_shift: OklabHue,
|
||||
}
|
||||
|
||||
impl ColorSchemeBuilder {
|
||||
/// Returns a builder for the provided hue, in degrees.
|
||||
#[must_use]
|
||||
pub fn from_hue(hue: impl Into<OklabHue>) -> Self {
|
||||
Self::new(ColorSource::new(hue, 0.8))
|
||||
}
|
||||
|
||||
/// Returns a builder for the provided primary color.
|
||||
#[must_use]
|
||||
pub fn new(primary: ColorSource) -> Self {
|
||||
pub fn new(primary: impl ProtoColor) -> Self {
|
||||
Self {
|
||||
primary,
|
||||
primary: primary.into_source(ZeroToOne::new(0.8)),
|
||||
secondary: None,
|
||||
tertiary: None,
|
||||
error: None,
|
||||
neutral: None,
|
||||
neutral_variant: None,
|
||||
hue_shift: 30.,
|
||||
hue_shift: OklabHue::new(30.),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1615,7 +1707,8 @@ impl ColorSchemeBuilder {
|
|||
}
|
||||
|
||||
fn generate_tertiary(&self, secondary: ColorSource) -> ColorSource {
|
||||
let hue_shift = (secondary.hue - self.primary.hue).into_degrees().signum() * self.hue_shift;
|
||||
let hue_shift = (secondary.hue - self.primary.hue).into_degrees().signum()
|
||||
* self.hue_shift.into_degrees();
|
||||
ColorSource {
|
||||
hue: self.primary.hue - hue_shift,
|
||||
saturation: self.primary.saturation / 3.,
|
||||
|
|
@ -1644,10 +1737,59 @@ impl ColorSchemeBuilder {
|
|||
fn generate_neutral_variant(&self) -> ColorSource {
|
||||
ColorSource {
|
||||
hue: self.primary.hue,
|
||||
saturation: ZeroToOne::new(0.1),
|
||||
saturation: self.primary.saturation / 10.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the secondary color and returns self.
|
||||
///
|
||||
/// If `secondary` doesn't specify a saturation, a saturation value that is
|
||||
/// 50% of the primary saturation will be picked.
|
||||
#[must_use]
|
||||
pub fn secondary(mut self, secondary: impl ProtoColor) -> Self {
|
||||
self.secondary = Some(secondary.into_source(self.primary.saturation / 2.));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the tertiary color and returns self.
|
||||
///
|
||||
/// If `tertiary` doesn't specify a saturation, a saturation value that is
|
||||
/// 33% of the primary saturation will be picked.
|
||||
#[must_use]
|
||||
pub fn tertiary(mut self, tertiary: impl ProtoColor) -> Self {
|
||||
self.secondary = Some(tertiary.into_source(self.primary.saturation / 3.));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the neutral color and returns self.
|
||||
///
|
||||
/// If `neutral` doesn't specify a saturation, a saturation of 1%.
|
||||
#[must_use]
|
||||
pub fn neutral(mut self, neutral: impl ProtoColor) -> Self {
|
||||
self.neutral = Some(neutral.into_source(0.01));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the neutral color and returns self.
|
||||
///
|
||||
/// If `neutral_variant` doesn't specify a saturation, a saturation value
|
||||
/// that is 10% of the primary saturation will be picked.
|
||||
#[must_use]
|
||||
pub fn neutral_variant(mut self, neutral_variant: impl ProtoColor) -> Self {
|
||||
self.neutral_variant = Some(neutral_variant.into_source(self.primary.saturation / 10.));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the amount the hue component is shifted when auto-generating colors
|
||||
/// to fill in the palette.
|
||||
///
|
||||
/// The default hue shift is 30 degrees.
|
||||
#[must_use]
|
||||
pub fn hue_shift(mut self, hue_shift: impl Into<OklabHue>) -> Self {
|
||||
self.hue_shift = hue_shift.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a color scheme from the provided colors, generating any
|
||||
/// unspecified colors.
|
||||
#[must_use]
|
||||
|
|
@ -1671,6 +1813,69 @@ impl ColorSchemeBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/// A type that can be interpretted as a hue or hue and saturation.
|
||||
pub trait ProtoColor: Sized {
|
||||
/// Returns the hue of this prototype color.
|
||||
#[must_use]
|
||||
fn hue(&self) -> OklabHue;
|
||||
/// Returns the saturation of this prototype color, if available.
|
||||
#[must_use]
|
||||
fn saturation(&self) -> Option<ZeroToOne>;
|
||||
|
||||
/// Returns a color source built from this prototype color
|
||||
#[must_use]
|
||||
fn into_source(self, saturation_if_not_provided: impl Into<ZeroToOne>) -> ColorSource {
|
||||
let saturation = self
|
||||
.saturation()
|
||||
.unwrap_or_else(|| saturation_if_not_provided.into());
|
||||
ColorSource::new(self.hue(), saturation)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoColor for f32 {
|
||||
fn hue(&self) -> OklabHue {
|
||||
(*self).into()
|
||||
}
|
||||
|
||||
fn saturation(&self) -> Option<ZeroToOne> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoColor for OklabHue {
|
||||
fn hue(&self) -> OklabHue {
|
||||
*self
|
||||
}
|
||||
|
||||
fn saturation(&self) -> Option<ZeroToOne> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoColor for ColorSource {
|
||||
fn hue(&self) -> OklabHue {
|
||||
self.hue
|
||||
}
|
||||
|
||||
fn saturation(&self) -> Option<ZeroToOne> {
|
||||
Some(self.saturation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Hue, Saturation> ProtoColor for (Hue, Saturation)
|
||||
where
|
||||
Hue: Into<OklabHue> + Copy,
|
||||
Saturation: Into<ZeroToOne> + Copy,
|
||||
{
|
||||
fn hue(&self) -> OklabHue {
|
||||
self.0.into()
|
||||
}
|
||||
|
||||
fn saturation(&self) -> Option<ZeroToOne> {
|
||||
Some(self.1.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A color scheme for a Gooey application.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ColorScheme {
|
||||
|
|
@ -1691,20 +1896,14 @@ pub struct ColorScheme {
|
|||
impl ColorScheme {
|
||||
/// Returns a generated color scheme based on a `primary` color.
|
||||
#[must_use]
|
||||
pub fn from_primary(primary: ColorSource) -> Self {
|
||||
pub fn from_primary(primary: impl ProtoColor) -> Self {
|
||||
ColorSchemeBuilder::new(primary).build()
|
||||
}
|
||||
|
||||
/// Returns a generated color scheme based on a `primary` hue, in degrees.
|
||||
#[must_use]
|
||||
pub fn from_primary_hue(hue: impl Into<OklabHue>) -> Self {
|
||||
ColorSchemeBuilder::from_hue(hue).build()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorScheme {
|
||||
fn default() -> Self {
|
||||
Self::from_primary_hue(138.5)
|
||||
Self::from_primary(138.5)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@ define_components! {
|
|||
SurfaceColor(Color, "surface_color", .surface.color)
|
||||
/// The [`Color`] to use when rendering text.
|
||||
TextColor(Color, "text_color", .surface.on_color)
|
||||
/// The [`Color`] to use when rendering text in a more subdued tone.
|
||||
TextColorVariant(Color, "text_color_variant", .surface.on_color_variant)
|
||||
/// A [`Color`] to be used as a highlight color.
|
||||
HighlightColor(Color,"highlight_color",.primary.color.with_alpha(128))
|
||||
/// Intrinsic, uniform padding for a widget.
|
||||
|
|
@ -122,6 +124,8 @@ define_components! {
|
|||
AutoFocusableControls(FocusableWidgets, "focus", FocusableWidgets::default())
|
||||
/// A [`Color`] to be used as the background color of a widget.
|
||||
WidgetBackground(Color, "widget_backgrond_color", Color::CLEAR_WHITE)
|
||||
/// A [`Color`] to be used to accent a widget.
|
||||
WidgetAccentColor(Color, "widget_accent_color", .primary.color)
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
OutlineColor(Color, "outline_color", .surface.outline)
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
|
|
|
|||
12
src/tick.rs
12
src/tick.rs
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use kludgine::app::winit::event::KeyEvent;
|
||||
|
|
@ -9,6 +9,7 @@ use kludgine::figures::Point;
|
|||
use kludgine::figures::units::Px;
|
||||
|
||||
use crate::context::WidgetContext;
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::Dynamic;
|
||||
use crate::widget::{EventHandling, HANDLED, IGNORED};
|
||||
|
||||
|
|
@ -138,9 +139,7 @@ struct TickData {
|
|||
|
||||
impl TickData {
|
||||
fn state(&self) -> MutexGuard<'_, TickState> {
|
||||
self.state
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
self.state.lock().ignore_poison()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,10 +188,7 @@ where
|
|||
while state.keep_running {
|
||||
let current_frame = data.rendered_frame.load(Ordering::Acquire);
|
||||
if state.frame == current_frame {
|
||||
state = data
|
||||
.sync
|
||||
.wait(state)
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
state = data.sync.wait(state).ignore_poison();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
334
src/tree.rs
334
src/tree.rs
|
|
@ -1,15 +1,18 @@
|
|||
use std::mem;
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use alot::{LotId, Lots};
|
||||
use kludgine::figures::units::Px;
|
||||
use kludgine::figures::{Point, Rect};
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{Point, Rect, Size};
|
||||
|
||||
use crate::context::WindowHandle;
|
||||
use crate::styles::{Styles, ThemePair, VisualOrder};
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::Value;
|
||||
use crate::widget::{ManagedWidget, WidgetId, WidgetInstance};
|
||||
use crate::window::ThemeMode;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Tree {
|
||||
|
|
@ -22,7 +25,7 @@ impl Tree {
|
|||
widget: WidgetInstance,
|
||||
parent: Option<&ManagedWidget>,
|
||||
) -> ManagedWidget {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
let id = widget.id();
|
||||
let (effective_styles, parent_id) = if let Some(parent) = parent {
|
||||
(
|
||||
|
|
@ -36,6 +39,7 @@ impl Tree {
|
|||
widget: widget.clone(),
|
||||
children: Vec::new(),
|
||||
parent: parent_id,
|
||||
last_layout_query: None,
|
||||
layout: None,
|
||||
associated_styles: None,
|
||||
effective_styles,
|
||||
|
|
@ -53,12 +57,8 @@ impl Tree {
|
|||
let parent = &mut data.nodes[parent];
|
||||
parent.children.push(node_id);
|
||||
}
|
||||
if let Some(next_focus) = widget
|
||||
.next_focus()
|
||||
.and_then(|id| data.nodes_by_id.get(&id))
|
||||
.copied()
|
||||
{
|
||||
data.previous_focuses.insert(next_focus, node_id);
|
||||
if let Some(next_focus) = widget.next_focus() {
|
||||
data.previous_focuses.insert(next_focus, id);
|
||||
}
|
||||
ManagedWidget {
|
||||
node_id,
|
||||
|
|
@ -68,7 +68,7 @@ impl Tree {
|
|||
}
|
||||
|
||||
pub fn remove_child(&self, child: &ManagedWidget, parent: &ManagedWidget) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.remove_child(child.node_id, parent.node_id);
|
||||
|
||||
if child.widget.is_default() {
|
||||
|
|
@ -80,7 +80,7 @@ impl Tree {
|
|||
}
|
||||
|
||||
pub(crate) fn set_layout(&self, widget: LotId, rect: Rect<Px>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
|
||||
let node = &mut data.nodes[widget];
|
||||
node.layout = Some(rect);
|
||||
|
|
@ -98,26 +98,65 @@ impl Tree {
|
|||
}
|
||||
|
||||
pub(crate) fn layout(&self, widget: LotId) -> Option<Rect<Px>> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.nodes.get(widget).and_then(|widget| widget.layout)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_render_order(&self) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.render_order.clear();
|
||||
pub(crate) fn new_frame(&self, invalidations: impl IntoIterator<Item = WidgetId>) {
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.render_info.clear();
|
||||
|
||||
for id in invalidations {
|
||||
let Some(id) = data.nodes_by_id.get(&id).copied() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
data.invalidate(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn note_widget_rendered(&self, widget: LotId) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.render_order.push(widget);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
let Some(layout) = data.nodes.get(widget).and_then(|node| node.layout) else {
|
||||
return;
|
||||
};
|
||||
data.render_info.push(widget, layout);
|
||||
}
|
||||
|
||||
pub(crate) fn reset_child_layouts(&self, parent: LotId) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let children = data.nodes[parent].children.clone();
|
||||
for child in children {
|
||||
data.nodes.get_mut(child).expect("missing widget").layout = None;
|
||||
pub(crate) fn begin_layout(
|
||||
&self,
|
||||
parent: LotId,
|
||||
constraints: Size<ConstraintLimit>,
|
||||
) -> Option<Size<UPx>> {
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
|
||||
let node = &mut data.nodes[parent];
|
||||
if let Some(cached_layout) = &node.last_layout_query {
|
||||
if constraints.width.max() < cached_layout.constraints.width.max()
|
||||
&& constraints.height.max() < cached_layout.constraints.height.max()
|
||||
{
|
||||
return Some(cached_layout.size);
|
||||
}
|
||||
|
||||
node.last_layout_query = None;
|
||||
}
|
||||
|
||||
let children = node.children.clone();
|
||||
for child in children {
|
||||
data.invalidate(child, false);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn persist_layout(
|
||||
&self,
|
||||
id: LotId,
|
||||
constraints: Size<ConstraintLimit>,
|
||||
size: Size<UPx>,
|
||||
) {
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.nodes[id].last_layout_query = Some(CachedLayoutQuery { constraints, size });
|
||||
}
|
||||
|
||||
pub(crate) fn visually_ordered_children(
|
||||
|
|
@ -125,7 +164,7 @@ impl Tree {
|
|||
parent: LotId,
|
||||
order: VisualOrder,
|
||||
) -> Vec<ManagedWidget> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
let node = &data.nodes[parent];
|
||||
let mut unordered = node.children.clone();
|
||||
let mut ordered = Vec::<ManagedWidget>::with_capacity(unordered.len());
|
||||
|
|
@ -182,89 +221,103 @@ impl Tree {
|
|||
}
|
||||
|
||||
pub(crate) fn effective_styles(&self, id: LotId) -> Styles {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.nodes[id].effective_styles.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> HoverResults {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
let hovered = new_hover
|
||||
.map(|new_hover| data.widget_hierarchy(new_hover.node_id, self))
|
||||
.unwrap_or_default();
|
||||
let unhovered = match data.update_tracked_widget(new_hover, self, |data| &mut data.hover) {
|
||||
Ok(Some(old_hover)) => {
|
||||
let mut old_hovered = data.widget_hierarchy(old_hover.node_id, self);
|
||||
// For any widgets that were shared, remove them, as they don't
|
||||
// need to have their events fired again.
|
||||
let mut new_index = 0;
|
||||
while !old_hovered.is_empty() && old_hovered.get(0) == hovered.get(new_index) {
|
||||
old_hovered.remove(0);
|
||||
new_index += 1;
|
||||
let unhovered =
|
||||
match data.update_tracked_widget(new_hover.map(ManagedWidget::id), self, |data| {
|
||||
&mut data.hover
|
||||
}) {
|
||||
Ok(Some(old_hover)) => {
|
||||
let mut old_hovered = data.widget_hierarchy(old_hover.node_id, self);
|
||||
// For any widgets that were shared, remove them, as they don't
|
||||
// need to have their events fired again.
|
||||
let mut new_index = 0;
|
||||
while !old_hovered.is_empty() && old_hovered.get(0) == hovered.get(new_index) {
|
||||
old_hovered.remove(0);
|
||||
new_index += 1;
|
||||
}
|
||||
old_hovered
|
||||
}
|
||||
old_hovered
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
_ => Vec::new(),
|
||||
};
|
||||
HoverResults { unhovered, hovered }
|
||||
}
|
||||
|
||||
pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
pub fn focus(&self, new_focus: Option<WidgetId>) -> Result<Option<ManagedWidget>, ()> {
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.update_tracked_widget(new_focus, self, |data| &mut data.focus)
|
||||
}
|
||||
|
||||
pub fn previous_focus(&self, focus: WidgetId) -> Option<ManagedWidget> {
|
||||
let data = self.data.lock().ignore_poison();
|
||||
let previous = *data.previous_focuses.get(&focus)?;
|
||||
data.widget_from_id(previous, self)
|
||||
}
|
||||
|
||||
pub fn activate(
|
||||
&self,
|
||||
new_active: Option<&ManagedWidget>,
|
||||
) -> Result<Option<ManagedWidget>, ()> {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.update_tracked_widget(new_active, self, |data| &mut data.active)
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.update_tracked_widget(new_active.map(ManagedWidget::id), self, |data| {
|
||||
&mut data.active
|
||||
})
|
||||
}
|
||||
|
||||
pub fn widget(&self, id: WidgetId) -> Option<ManagedWidget> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.widget_from_id(id, self)
|
||||
}
|
||||
|
||||
pub(crate) fn widget_from_node(&self, id: LotId) -> Option<ManagedWidget> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.widget_from_node(id, self)
|
||||
}
|
||||
|
||||
pub(crate) fn is_enabled(&self, mut id: LotId, context: &WindowHandle) -> bool {
|
||||
let data = self.data.lock().ignore_poison();
|
||||
loop {
|
||||
let Some(node) = data.nodes.get(id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !node.widget.enabled(context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(parent) = node.parent else { break };
|
||||
|
||||
id = parent;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn active_widget(&self) -> Option<LotId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.active
|
||||
self.data.lock().ignore_poison().active
|
||||
}
|
||||
|
||||
pub(crate) fn hovered_widget(&self) -> Option<LotId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.hover
|
||||
self.data.lock().ignore_poison().hover
|
||||
}
|
||||
|
||||
pub(crate) fn default_widget(&self) -> Option<LotId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.defaults
|
||||
.last()
|
||||
.copied()
|
||||
self.data.lock().ignore_poison().defaults.last().copied()
|
||||
}
|
||||
|
||||
pub(crate) fn escape_widget(&self) -> Option<LotId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.escapes
|
||||
.last()
|
||||
.copied()
|
||||
self.data.lock().ignore_poison().escapes.last().copied()
|
||||
}
|
||||
|
||||
pub(crate) fn is_hovered(&self, id: LotId) -> bool {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
let mut search = data.hover;
|
||||
while let Some(hovered) = search {
|
||||
if hovered == id {
|
||||
|
|
@ -277,42 +330,31 @@ impl Tree {
|
|||
}
|
||||
|
||||
pub(crate) fn focused_widget(&self) -> Option<LotId> {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.focus
|
||||
self.data.lock().ignore_poison().focus
|
||||
}
|
||||
|
||||
pub(crate) fn widgets_at_point(&self, point: Point<Px>) -> Vec<ManagedWidget> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut hits = Vec::new();
|
||||
for id in data.render_order.iter().rev() {
|
||||
if let Some(last_rendered) = data.nodes.get(*id).and_then(|widget| widget.layout) {
|
||||
if last_rendered.contains(point) {
|
||||
hits.push(data.widget_from_node(*id, self).expect("just accessed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits
|
||||
pub(crate) fn widgets_under_point(&self, point: Point<Px>) -> Vec<ManagedWidget> {
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.render_info.widgets_under_point(point, &data, self)
|
||||
}
|
||||
|
||||
pub(crate) fn parent(&self, id: LotId) -> Option<LotId> {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
data.nodes.get(id).expect("missing widget").parent
|
||||
}
|
||||
|
||||
pub(crate) fn attach_styles(&self, id: LotId, styles: Value<Styles>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.attach_styles(id, styles);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme(&self, id: LotId, theme: Value<ThemePair>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.nodes.get_mut(id).expect("missing widget").theme = Some(theme);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme_mode(&self, id: LotId, theme: Value<ThemeMode>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut data = self.data.lock().ignore_poison();
|
||||
data.nodes.get_mut(id).expect("missing widget").theme_mode = Some(theme);
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +362,7 @@ impl Tree {
|
|||
&self,
|
||||
id: LotId,
|
||||
) -> (Styles, Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let data = self.data.lock().ignore_poison();
|
||||
let node = data.nodes.get(id).expect("missing widget");
|
||||
(
|
||||
node.effective_styles.clone(),
|
||||
|
|
@ -328,6 +370,13 @@ impl Tree {
|
|||
node.theme_mode.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn invalidate(&self, id: LotId, include_hierarchy: bool) {
|
||||
self.data
|
||||
.lock()
|
||||
.ignore_poison()
|
||||
.invalidate(id, include_hierarchy);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HoverResults {
|
||||
|
|
@ -344,8 +393,8 @@ struct TreeData {
|
|||
hover: Option<LotId>,
|
||||
defaults: Vec<LotId>,
|
||||
escapes: Vec<LotId>,
|
||||
render_order: Vec<LotId>,
|
||||
previous_focuses: AHashMap<LotId, LotId>,
|
||||
render_info: RenderInfo,
|
||||
previous_focuses: AHashMap<WidgetId, WidgetId>,
|
||||
}
|
||||
|
||||
impl TreeData {
|
||||
|
|
@ -408,17 +457,16 @@ impl TreeData {
|
|||
parent.children.remove(index);
|
||||
let mut detached_nodes = removed_node.children;
|
||||
|
||||
if let Some(next_focus) = removed_node
|
||||
.widget
|
||||
.next_focus()
|
||||
.and_then(|id| self.nodes_by_id.get(&id))
|
||||
{
|
||||
self.previous_focuses.remove(next_focus);
|
||||
if let Some(next_focus) = removed_node.widget.next_focus() {
|
||||
self.previous_focuses.remove(&next_focus);
|
||||
}
|
||||
|
||||
while let Some(node) = detached_nodes.pop() {
|
||||
let mut node = self.nodes.remove(node).expect("detached node missing");
|
||||
self.nodes_by_id.remove(&node.widget.id());
|
||||
if let Some(next_focus) = node.widget.next_focus() {
|
||||
self.previous_focuses.remove(&next_focus);
|
||||
}
|
||||
detached_nodes.append(&mut node.children);
|
||||
}
|
||||
}
|
||||
|
|
@ -440,12 +488,13 @@ impl TreeData {
|
|||
|
||||
fn update_tracked_widget(
|
||||
&mut self,
|
||||
new_widget: Option<&ManagedWidget>,
|
||||
new_widget: Option<WidgetId>,
|
||||
tree: &Tree,
|
||||
property: impl FnOnce(&mut Self) -> &mut Option<LotId>,
|
||||
) -> Result<Option<ManagedWidget>, ()> {
|
||||
let new_widget = new_widget.and_then(|w| self.widget_from_id(w, tree));
|
||||
match (
|
||||
mem::replace(property(self), new_widget.map(|w| w.node_id)),
|
||||
mem::replace(property(self), new_widget.as_ref().map(|w| w.node_id)),
|
||||
new_widget,
|
||||
) {
|
||||
(Some(old_widget), Some(new_widget)) if old_widget == new_widget.node_id => Err(()),
|
||||
|
|
@ -453,17 +502,87 @@ impl TreeData {
|
|||
(None, _) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate(&mut self, id: LotId, include_hierarchy: bool) {
|
||||
let mut node = &mut self.nodes[id];
|
||||
while node.layout.is_some() {
|
||||
node.layout = None;
|
||||
node.last_layout_query = None;
|
||||
|
||||
let (true, Some(parent)) = (include_hierarchy, node.parent) else {
|
||||
break;
|
||||
};
|
||||
node = &mut self.nodes[parent];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Node {
|
||||
pub widget: WidgetInstance,
|
||||
pub children: Vec<LotId>,
|
||||
pub parent: Option<LotId>,
|
||||
pub layout: Option<Rect<Px>>,
|
||||
pub associated_styles: Option<Value<Styles>>,
|
||||
pub effective_styles: Styles,
|
||||
pub theme: Option<Value<ThemePair>>,
|
||||
pub theme_mode: Option<Value<ThemeMode>>,
|
||||
#[derive(Default)]
|
||||
struct RenderInfo {
|
||||
order: Vec<RenderArea>,
|
||||
}
|
||||
|
||||
impl RenderInfo {
|
||||
pub fn push(&mut self, node: LotId, region: Rect<Px>) {
|
||||
let area = RenderArea::new(node, region);
|
||||
self.order.push(area);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.order.clear();
|
||||
}
|
||||
|
||||
fn widgets_under_point(
|
||||
&self,
|
||||
point: Point<Px>,
|
||||
tree_data: &TreeData,
|
||||
tree: &Tree,
|
||||
) -> Vec<ManagedWidget> {
|
||||
// We pessimistically allocate a vector as if all widgets match, up to a
|
||||
// reasonable limit. This should ensure minimal allocations in all but
|
||||
// extreme circumstances where widgets are nested with a significant
|
||||
// amount of depth.
|
||||
let mut hits = Vec::with_capacity(self.order.len().min(256));
|
||||
for area in self.order.iter().rev() {
|
||||
if area.min.x <= point.x
|
||||
&& area.min.y <= point.y
|
||||
&& area.max.x >= point.x
|
||||
&& area.max.y >= point.y
|
||||
{
|
||||
let Some(widget) = tree_data.widget_from_node(area.node, tree) else {
|
||||
continue;
|
||||
};
|
||||
hits.push(widget);
|
||||
}
|
||||
}
|
||||
hits
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||
struct RenderArea {
|
||||
node: LotId,
|
||||
min: Point<Px>,
|
||||
max: Point<Px>,
|
||||
}
|
||||
|
||||
impl RenderArea {
|
||||
fn new(node: LotId, area: Rect<Px>) -> Self {
|
||||
let (min, max) = area.extents();
|
||||
Self { node, min, max }
|
||||
}
|
||||
}
|
||||
|
||||
struct Node {
|
||||
widget: WidgetInstance,
|
||||
children: Vec<LotId>,
|
||||
parent: Option<LotId>,
|
||||
layout: Option<Rect<Px>>,
|
||||
last_layout_query: Option<CachedLayoutQuery>,
|
||||
associated_styles: Option<Value<Styles>>,
|
||||
effective_styles: Styles,
|
||||
theme: Option<Value<ThemePair>>,
|
||||
theme_mode: Option<Value<ThemeMode>>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
|
|
@ -475,3 +594,8 @@ impl Node {
|
|||
effective_styles
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedLayoutQuery {
|
||||
constraints: Size<ConstraintLimit>,
|
||||
size: Size<UPx>,
|
||||
}
|
||||
|
|
|
|||
15
src/utils.rs
15
src/utils.rs
|
|
@ -1,5 +1,5 @@
|
|||
use std::ops::Deref;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{OnceLock, PoisonError};
|
||||
|
||||
use kludgine::app::winit::event::Modifiers;
|
||||
use kludgine::app::winit::keyboard::ModifiersState;
|
||||
|
|
@ -129,3 +129,16 @@ impl<T> Deref for Lazy<T> {
|
|||
self.once.get_or_init(self.init)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IgnorePoison {
|
||||
type Unwrapped;
|
||||
fn ignore_poison(self) -> Self::Unwrapped;
|
||||
}
|
||||
|
||||
impl<T> IgnorePoison for Result<T, PoisonError<T>> {
|
||||
type Unwrapped = T;
|
||||
|
||||
fn ignore_poison(self) -> Self::Unwrapped {
|
||||
self.map_or_else(PoisonError::into_inner, |g| g)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
261
src/value.rs
261
src/value.rs
|
|
@ -5,15 +5,19 @@ use std::fmt::{Debug, Display};
|
|||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, TryLockError};
|
||||
use std::task::{Poll, Waker};
|
||||
use std::thread::ThreadId;
|
||||
|
||||
use ahash::AHashSet;
|
||||
use intentional::Assert;
|
||||
|
||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||
use crate::context::{WidgetContext, WindowHandle};
|
||||
use crate::utils::WithClone;
|
||||
use crate::utils::{IgnorePoison, WithClone};
|
||||
use crate::widget::{WidgetId, WidgetInstance};
|
||||
use crate::widgets::{Input, Switcher};
|
||||
|
||||
/// An instance of a value that provides APIs to observe and react to its
|
||||
/// contents.
|
||||
|
|
@ -30,15 +34,84 @@ impl<T> Dynamic<T> {
|
|||
generation: Generation::default(),
|
||||
},
|
||||
callbacks: Vec::new(),
|
||||
windows: Vec::new(),
|
||||
windows: AHashSet::new(),
|
||||
readers: 0,
|
||||
wakers: Vec::new(),
|
||||
widgets: AHashSet::new(),
|
||||
}),
|
||||
during_callback_state: Mutex::default(),
|
||||
sync: AssertUnwindSafe(Condvar::new()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns a new dynamic that has its contents linked with `self` by the
|
||||
/// pair of mapping functions provided.
|
||||
///
|
||||
/// When the returned dynamic is updated, `r_into_t` will be invoked. This
|
||||
/// function accepts `&R` and can return `T`, or `Option<T>`. If a value is
|
||||
/// produced, `self` will be updated with the new value.
|
||||
///
|
||||
/// When `self` is updated, `t_into_r` will be invoked. This function
|
||||
/// accepts `&T` and can return `R` or `Option<R>`. If a value is produced,
|
||||
/// the returned dynamic will be updated with the new value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if calling `t_into_r` with the current contents of
|
||||
/// the Dynamic produces a `None` value. This requirement is only for the
|
||||
/// first invocation, and it is guaranteed to occur before this function
|
||||
/// returns.
|
||||
pub fn linked<R, TIntoR, TIntoRResult, RIntoT, RIntoTResult>(
|
||||
&self,
|
||||
mut t_into_r: TIntoR,
|
||||
mut r_into_t: RIntoT,
|
||||
) -> Dynamic<R>
|
||||
where
|
||||
T: PartialEq + Send + 'static,
|
||||
R: PartialEq + Send + 'static,
|
||||
TIntoRResult: Into<Option<R>> + Send + 'static,
|
||||
RIntoTResult: Into<Option<T>> + Send + 'static,
|
||||
TIntoR: FnMut(&T) -> TIntoRResult + Send + 'static,
|
||||
RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static,
|
||||
{
|
||||
let initial_r = self
|
||||
.map_ref(&mut t_into_r)
|
||||
.into()
|
||||
.expect("t_into_r must succeed with the current value");
|
||||
let r = Dynamic::new(initial_r);
|
||||
r.with_clone(move |r| {
|
||||
self.for_each(move |t| {
|
||||
if let Some(update) = t_into_r(t).into() {
|
||||
let _result = r.try_update(update);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.with_clone(|t| {
|
||||
r.with_for_each(move |r| {
|
||||
if let Some(update) = r_into_t(r).into() {
|
||||
let _result = t.try_update(update);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a [linked](Self::linked) dynamic containing a `String`.
|
||||
///
|
||||
/// When `self` is updated, [`ToString::to_string()`] will be called to
|
||||
/// produce a new string value to store in the returned dynamic.
|
||||
///
|
||||
/// When the returned dynamic is updated, [`str::parse`](std::str) is called
|
||||
/// to produce a new `T`. If an error is returned, `self` will not be
|
||||
/// updated. Otherwise, `self` will be updated with the produced value.
|
||||
#[must_use]
|
||||
pub fn linked_string(&self) -> Dynamic<String>
|
||||
where
|
||||
T: ToString + FromStr + PartialEq + Send + 'static,
|
||||
{
|
||||
self.linked(ToString::to_string, |s: &String| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Maps the contents with read-only access.
|
||||
///
|
||||
/// # Panics
|
||||
|
|
@ -158,6 +231,10 @@ impl<T> Dynamic<T> {
|
|||
self.0.redraw_when_changed(window);
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) {
|
||||
self.0.invalidate_when_changed(window, widget);
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// # Panics
|
||||
|
|
@ -181,7 +258,7 @@ impl<T> Dynamic<T> {
|
|||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
pub fn get_tracking_refresh(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
|
|
@ -189,6 +266,23 @@ impl<T> Dynamic<T> {
|
|||
self.get()
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
context.invalidate_when_changed(self);
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Returns the currently stored value, replacing the current contents with
|
||||
/// `T::default()`.
|
||||
///
|
||||
|
|
@ -370,6 +464,15 @@ impl<T> Dynamic<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Dynamic<WidgetInstance> {
|
||||
/// Returns a new [`Switcher`] widget whose contents is the value of this
|
||||
/// dynamic.
|
||||
#[must_use]
|
||||
pub fn switcher(self) -> Switcher {
|
||||
Switcher::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Dynamic<T>
|
||||
where
|
||||
T: Default,
|
||||
|
|
@ -409,11 +512,7 @@ struct DynamicMutexGuard<'a, T> {
|
|||
|
||||
impl<'a, T> Drop for DynamicMutexGuard<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
let mut during_state = self
|
||||
.dynamic
|
||||
.during_callback_state
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut during_state = self.dynamic.during_callback_state.lock().ignore_poison();
|
||||
*during_state = None;
|
||||
drop(during_state);
|
||||
self.dynamic.sync.notify_all();
|
||||
|
|
@ -450,10 +549,7 @@ struct DynamicData<T> {
|
|||
|
||||
impl<T> DynamicData<T> {
|
||||
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
|
||||
let mut during_sync = self
|
||||
.during_callback_state
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut during_sync = self.during_callback_state.lock().ignore_poison();
|
||||
|
||||
let current_thread_id = std::thread::current().id();
|
||||
let guard = loop {
|
||||
|
|
@ -466,10 +562,7 @@ impl<T> DynamicData<T> {
|
|||
return Err(DeadlockError)
|
||||
}
|
||||
Some(_) => {
|
||||
during_sync = self
|
||||
.sync
|
||||
.wait(during_sync)
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
during_sync = self.sync.wait(during_sync).ignore_poison();
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
|
|
@ -487,7 +580,12 @@ impl<T> DynamicData<T> {
|
|||
|
||||
pub fn redraw_when_changed(&self, window: WindowHandle) {
|
||||
let mut state = self.state().expect("deadlocked");
|
||||
state.windows.push(window);
|
||||
state.windows.insert(window);
|
||||
}
|
||||
|
||||
pub fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) {
|
||||
let mut state = self.state().expect("deadlocked");
|
||||
state.widgets.insert((window, widget));
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Result<GenerationalValue<T>, DeadlockError>
|
||||
|
|
@ -579,7 +677,8 @@ impl Display for DeadlockError {
|
|||
struct State<T> {
|
||||
wrapped: GenerationalValue<T>,
|
||||
callbacks: Vec<Box<dyn ValueCallback<T>>>,
|
||||
windows: Vec<WindowHandle>,
|
||||
windows: AHashSet<WindowHandle>,
|
||||
widgets: AHashSet<(WindowHandle, WidgetId)>,
|
||||
wakers: Vec<Waker>,
|
||||
readers: usize,
|
||||
}
|
||||
|
|
@ -591,7 +690,10 @@ impl<T> State<T> {
|
|||
for callback in &mut self.callbacks {
|
||||
callback.update(&self.wrapped);
|
||||
}
|
||||
for window in self.windows.drain(..) {
|
||||
for (window, widget) in self.widgets.drain() {
|
||||
window.invalidate(widget);
|
||||
}
|
||||
for window in self.windows.drain() {
|
||||
window.redraw();
|
||||
}
|
||||
for waker in self.wakers.drain(..) {
|
||||
|
|
@ -716,6 +818,45 @@ impl<T> DynamicReader<T> {
|
|||
value
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// This function marks the currently stored value as being read.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracking_refresh(&mut self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.source.redraw_when_changed(context.handle());
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// This function marks the currently stored value as being read.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn get_tracking_invalidate(&mut self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.source
|
||||
.invalidate_when_changed(context.handle(), context.widget().id());
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Blocks the current thread until the contained value has been updated or
|
||||
/// there are no remaining writers for the value.
|
||||
///
|
||||
|
|
@ -726,11 +867,7 @@ impl<T> DynamicReader<T> {
|
|||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
pub fn block_until_updated(&mut self) -> bool {
|
||||
let mut deadlock_state = self
|
||||
.source
|
||||
.during_callback_state
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut deadlock_state = self.source.during_callback_state.lock().ignore_poison();
|
||||
assert!(
|
||||
deadlock_state
|
||||
.as_ref()
|
||||
|
|
@ -739,11 +876,7 @@ impl<T> DynamicReader<T> {
|
|||
"deadlocked"
|
||||
);
|
||||
loop {
|
||||
let state = self
|
||||
.source
|
||||
.state
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
let state = self.source.state.lock().ignore_poison();
|
||||
if state.wrapped.generation != self.read_generation {
|
||||
return true;
|
||||
} else if state.readers == Arc::strong_count(&self.source) {
|
||||
|
|
@ -752,11 +885,7 @@ impl<T> DynamicReader<T> {
|
|||
drop(state);
|
||||
|
||||
// Wait for a notification of a change, which is synch
|
||||
deadlock_state = self
|
||||
.source
|
||||
.sync
|
||||
.wait(deadlock_state)
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
deadlock_state = self.source.sync.wait(deadlock_state).ignore_poison();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -909,6 +1038,21 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A type that can be the source of a [`Switcher`] widget.
|
||||
pub trait Switchable<T>: IntoDynamic<T> + Sized {
|
||||
/// Returns a new [`Switcher`] whose contents is the result of invoking
|
||||
/// `map` each time `self` is updated.
|
||||
fn switcher<F>(self, map: F) -> Switcher
|
||||
where
|
||||
F: FnMut(&T, &Dynamic<T>) -> WidgetInstance + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
Switcher::mapping(self, map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, W> Switchable<T> for W where W: IntoDynamic<T> {}
|
||||
|
||||
/// A value that may be either constant or dynamic.
|
||||
#[derive(Debug)]
|
||||
pub enum Value<T> {
|
||||
|
|
@ -936,7 +1080,11 @@ impl<T> Value<T> {
|
|||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn map_tracked<R>(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R {
|
||||
pub fn map_tracking_redraw<R>(
|
||||
&self,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
map: impl FnOnce(&T) -> R,
|
||||
) -> R {
|
||||
match self {
|
||||
Value::Constant(value) => map(value),
|
||||
Value::Dynamic(dynamic) => {
|
||||
|
|
@ -946,6 +1094,24 @@ impl<T> Value<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents to `map` and returns the result.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn map_tracking_invalidate<R>(
|
||||
&self,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
map: impl FnOnce(&T) -> R,
|
||||
) -> R {
|
||||
match self {
|
||||
Value::Constant(value) => map(value),
|
||||
Value::Dynamic(dynamic) => {
|
||||
context.invalidate_when_changed(dynamic);
|
||||
dynamic.map_ref(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents with exclusive access and returns the result.
|
||||
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||
match self {
|
||||
|
|
@ -984,7 +1150,7 @@ impl<T> Value<T> {
|
|||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.map_tracked(context, Clone::clone)
|
||||
self.map_tracking_redraw(context, Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns the current generation of the data stored, if the contained
|
||||
|
|
@ -1004,6 +1170,15 @@ impl<T> Value<T> {
|
|||
context.redraw_when_changed(dynamic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the widget for redraw when this value is updated.
|
||||
///
|
||||
/// This function has no effect if the value is constant.
|
||||
pub fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) {
|
||||
if let Value::Dynamic(dynamic) = self {
|
||||
context.invalidate_when_changed(dynamic);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for Value<T>
|
||||
where
|
||||
|
|
@ -1155,7 +1330,7 @@ macro_rules! impl_tuple_for_each {
|
|||
move |$var: &$type| {
|
||||
$(let $rvar = $rvar.lock();)+
|
||||
let mut for_each =
|
||||
for_each.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
for_each.lock().ignore_poison();
|
||||
(for_each)(($(&$avar,)+));
|
||||
}
|
||||
}));
|
||||
|
|
@ -1209,3 +1384,13 @@ macro_rules! impl_tuple_map_each {
|
|||
}
|
||||
|
||||
impl_all_tuples!(impl_tuple_map_each);
|
||||
|
||||
/// A type that can be converted into a [`Value<String>`].
|
||||
pub trait StringValue: IntoValue<String> + Sized {
|
||||
/// Returns this string as a text input widget.
|
||||
fn into_input(self) -> Input {
|
||||
Input::new(self.into_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StringValue for T where T: IntoValue<String> {}
|
||||
|
|
|
|||
148
src/widget.rs
148
src/widget.rs
|
|
@ -6,7 +6,7 @@ use std::fmt::Debug;
|
|||
use std::ops::{ControlFlow, Deref, DerefMut};
|
||||
use std::panic::UnwindSafe;
|
||||
use std::sync::atomic::{self, AtomicU64};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use alot::LotId;
|
||||
use kludgine::app::winit::event::{
|
||||
|
|
@ -16,14 +16,19 @@ use kludgine::figures::units::{Px, UPx};
|
|||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext, WindowHandle,
|
||||
};
|
||||
use crate::styles::{
|
||||
ContainerLevel, Dimension, DimensionRange, Edges, IntoComponentValue, NamedComponent, Styles,
|
||||
ThemePair, VisualOrder,
|
||||
};
|
||||
use crate::tree::Tree;
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widgets::{Align, Container, Expand, Resize, Scroll, Stack, Style};
|
||||
use crate::widgets::{
|
||||
Align, Button, Container, Expand, Resize, Scroll, Stack, Style, Themed, ThemedMode,
|
||||
};
|
||||
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
||||
use crate::{ConstraintLimit, Run};
|
||||
|
||||
|
|
@ -225,6 +230,18 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
|
|||
/// Returns the child widget.
|
||||
fn child_mut(&mut self) -> &mut WidgetRef;
|
||||
|
||||
/// Draws the background of the widget.
|
||||
///
|
||||
/// This is invoked before the wrapped widget is drawn.
|
||||
#[allow(unused_variables)]
|
||||
fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {}
|
||||
|
||||
/// Draws the foreground of the widget.
|
||||
///
|
||||
/// This is invoked after the wrapped widget is drawn.
|
||||
#[allow(unused_variables)]
|
||||
fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {}
|
||||
|
||||
/// Returns the rectangle that the child widget should occupy given
|
||||
/// `available_space`.
|
||||
#[allow(unused_variables)]
|
||||
|
|
@ -263,7 +280,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> WrappedLayout {
|
||||
Size::<UPx>::new(
|
||||
Size::new(
|
||||
available_space
|
||||
.width
|
||||
.fit_measured(size.width, context.gfx.scale()),
|
||||
|
|
@ -416,8 +433,12 @@ where
|
|||
context.gfx.fill(color);
|
||||
}
|
||||
|
||||
self.redraw_background(context);
|
||||
|
||||
let child = self.child_mut().mounted(&mut context.as_event_context());
|
||||
context.for_other(&child).redraw();
|
||||
|
||||
self.redraw_foreground(context);
|
||||
}
|
||||
|
||||
fn layout(
|
||||
|
|
@ -561,6 +582,19 @@ pub trait MakeWidget: Sized {
|
|||
self.make_widget().with_next_focus(next_focus)
|
||||
}
|
||||
|
||||
/// Sets this widget to be enabled/disabled based on `enabled` and returns
|
||||
/// self.
|
||||
///
|
||||
/// If this widget is disabled, all children widgets will also be disabled.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function can only be called when one instance of the widget exists.
|
||||
/// If any clones exist, a panic will occur.
|
||||
fn with_enabled(self, enabled: impl IntoValue<bool>) -> WidgetInstance {
|
||||
self.make_widget().with_enabled(enabled)
|
||||
}
|
||||
|
||||
/// Sets this widget as a "default" widget.
|
||||
///
|
||||
/// Default widgets are automatically activated when the user signals they
|
||||
|
|
@ -624,9 +658,12 @@ pub trait MakeWidget: Sized {
|
|||
|
||||
/// Resizes `self` to `width`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `width` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`]
|
||||
/// - [`Px`]
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
fn width(self, width: impl Into<DimensionRange>) -> Resize {
|
||||
Resize::from_width(width, self)
|
||||
|
|
@ -634,14 +671,22 @@ pub trait MakeWidget: Sized {
|
|||
|
||||
/// Resizes `self` to `height`.
|
||||
///
|
||||
/// `height` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `height` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`]
|
||||
/// - [`Px`]
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
fn height(self, height: impl Into<DimensionRange>) -> Resize {
|
||||
Resize::from_height(height, self)
|
||||
}
|
||||
|
||||
/// Returns this string as a clickable button.
|
||||
fn into_button(self) -> Button {
|
||||
Button::new(self)
|
||||
}
|
||||
|
||||
/// Aligns `self` to the center vertically and horizontally.
|
||||
#[must_use]
|
||||
fn centered(self) -> Align {
|
||||
|
|
@ -726,6 +771,16 @@ pub trait MakeWidget: Sized {
|
|||
fn pad_by(self, padding: impl IntoValue<Edges<Dimension>>) -> Container {
|
||||
self.contain().transparent().pad_by(padding)
|
||||
}
|
||||
|
||||
/// Applies `theme` to `self` and its children.
|
||||
fn themed(self, theme: impl IntoValue<ThemePair>) -> Themed {
|
||||
Themed::new(theme, self)
|
||||
}
|
||||
|
||||
/// Applies `mode` to `self` and its children.
|
||||
fn themed_mode(self, mode: impl IntoValue<ThemeMode>) -> ThemedMode {
|
||||
ThemedMode::new(mode, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can create a [`WidgetInstance`] with a preallocated
|
||||
|
|
@ -806,6 +861,7 @@ struct WidgetInstanceData {
|
|||
default: bool,
|
||||
cancel: bool,
|
||||
next_focus: Value<Option<WidgetId>>,
|
||||
enabled: Value<bool>,
|
||||
widget: Box<Mutex<dyn AnyWidget>>,
|
||||
}
|
||||
|
||||
|
|
@ -823,6 +879,7 @@ impl WidgetInstance {
|
|||
default: false,
|
||||
cancel: false,
|
||||
widget: Box::new(Mutex::new(widget)),
|
||||
enabled: Value::Constant(true),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -861,6 +918,23 @@ impl WidgetInstance {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets this widget to be enabled/disabled based on `enabled` and returns
|
||||
/// self.
|
||||
///
|
||||
/// If this widget is disabled, all children widgets will also be disabled.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function can only be called when one instance of the widget exists.
|
||||
/// If any clones exist, a panic will occur.
|
||||
#[must_use]
|
||||
pub fn with_enabled(mut self, enabled: impl IntoValue<bool>) -> WidgetInstance {
|
||||
let data = Arc::get_mut(&mut self.data)
|
||||
.expect("with_enabled can only be called on newly created widget instances");
|
||||
data.enabled = enabled.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets this widget as a "default" widget.
|
||||
///
|
||||
/// Default widgets are automatically activated when the user signals they
|
||||
|
|
@ -908,13 +982,9 @@ impl WidgetInstance {
|
|||
/// Locks the widget for exclusive access. Locking widgets should only be
|
||||
/// done for brief moments of time when you are certain no deadlocks can
|
||||
/// occur due to other widget locks being held.
|
||||
#[must_use]
|
||||
pub fn lock(&self) -> WidgetGuard<'_> {
|
||||
WidgetGuard(
|
||||
self.data
|
||||
.widget
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g),
|
||||
)
|
||||
WidgetGuard(self.data.widget.lock().ignore_poison())
|
||||
}
|
||||
|
||||
/// Runs this widget instance as an application.
|
||||
|
|
@ -946,6 +1016,13 @@ impl WidgetInstance {
|
|||
pub fn is_escape(&self) -> bool {
|
||||
self.data.cancel
|
||||
}
|
||||
|
||||
pub(crate) fn enabled(&self, context: &WindowHandle) -> bool {
|
||||
if let Value::Dynamic(dynamic) = &self.data.enabled {
|
||||
dynamic.redraw_when_changed(context.clone());
|
||||
}
|
||||
self.data.enabled.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<WidgetId> for WidgetInstance {
|
||||
|
|
@ -1041,6 +1118,11 @@ impl ManagedWidget {
|
|||
self.widget.lock()
|
||||
}
|
||||
|
||||
/// Invalidates this widget.
|
||||
pub fn invalidate(&self) {
|
||||
self.tree.invalidate(self.node_id, false);
|
||||
}
|
||||
|
||||
pub(crate) fn set_layout(&self, rect: Rect<Px>) {
|
||||
self.tree.set_layout(self.node_id, rect);
|
||||
}
|
||||
|
|
@ -1062,6 +1144,26 @@ impl ManagedWidget {
|
|||
.and_then(|next_focus| self.tree.widget(next_focus))
|
||||
}
|
||||
|
||||
/// Returns the widget to focus before this widget.
|
||||
///
|
||||
/// There is no direct way to set this value. This relationship is created
|
||||
/// automatically using [`MakeWidget::with_next_focus()`].
|
||||
#[must_use]
|
||||
pub fn previous_focus(&self) -> Option<ManagedWidget> {
|
||||
self.tree.previous_focus(self.id())
|
||||
}
|
||||
|
||||
/// Returns the next or previous focus target, if one was set using
|
||||
/// [`MakeWidget::with_next_focus()`].
|
||||
#[must_use]
|
||||
pub fn explicit_focus_target(&self, advance: bool) -> Option<ManagedWidget> {
|
||||
if advance {
|
||||
self.next_focus()
|
||||
} else {
|
||||
self.previous_focus()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the region that the widget was last rendered at.
|
||||
#[must_use]
|
||||
pub fn last_layout(&self) -> Option<Rect<Px>> {
|
||||
|
|
@ -1080,6 +1182,10 @@ impl ManagedWidget {
|
|||
self.tree.active_widget() == Some(self.node_id)
|
||||
}
|
||||
|
||||
pub(crate) fn enabled(&self, handle: &WindowHandle) -> bool {
|
||||
self.tree.is_enabled(self.node_id, handle)
|
||||
}
|
||||
|
||||
/// Returns true if this widget is currently the hovered widget.
|
||||
#[must_use]
|
||||
pub fn hovered(&self) -> bool {
|
||||
|
|
@ -1130,8 +1236,12 @@ impl ManagedWidget {
|
|||
self.tree.overriden_theme(self.node_id)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_child_layouts(&self) {
|
||||
self.tree.reset_child_layouts(self.node_id);
|
||||
pub(crate) fn begin_layout(&self, constraints: Size<ConstraintLimit>) -> Option<Size<UPx>> {
|
||||
self.tree.begin_layout(self.node_id, constraints)
|
||||
}
|
||||
|
||||
pub(crate) fn persist_layout(&self, constraints: Size<ConstraintLimit>, size: Size<UPx>) {
|
||||
self.tree.persist_layout(self.node_id, constraints, size);
|
||||
}
|
||||
|
||||
pub(crate) fn visually_ordered_children(&self, order: VisualOrder) -> Vec<ManagedWidget> {
|
||||
|
|
@ -1343,7 +1453,7 @@ impl AsRef<WidgetId> for WidgetRef {
|
|||
///
|
||||
/// Each [`WidgetInstance`] is guaranteed to have a unique [`WidgetId`] across
|
||||
/// the lifetime of an application.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Ord, PartialOrd)]
|
||||
pub struct WidgetId(u64);
|
||||
|
||||
impl WidgetId {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
mod align;
|
||||
pub mod button;
|
||||
mod canvas;
|
||||
pub mod checkbox;
|
||||
pub mod container;
|
||||
mod expand;
|
||||
mod input;
|
||||
|
|
@ -10,7 +11,7 @@ pub mod label;
|
|||
mod mode_switch;
|
||||
mod resize;
|
||||
pub mod scroll;
|
||||
mod slider;
|
||||
pub mod slider;
|
||||
mod space;
|
||||
pub mod stack;
|
||||
mod style;
|
||||
|
|
@ -21,11 +22,12 @@ mod tilemap;
|
|||
pub use align::Align;
|
||||
pub use button::Button;
|
||||
pub use canvas::Canvas;
|
||||
pub use checkbox::Checkbox;
|
||||
pub use container::Container;
|
||||
pub use expand::Expand;
|
||||
pub use input::Input;
|
||||
pub use label::Label;
|
||||
pub use mode_switch::ModeSwitch;
|
||||
pub use mode_switch::ThemedMode;
|
||||
pub use resize::Resize;
|
||||
pub use scroll::Scroll;
|
||||
pub use slider::Slider;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt::Debug;
|
||||
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, Point, Rect, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
use crate::styles::{Edges, FlexibleDimension};
|
||||
|
|
@ -125,15 +125,11 @@ impl FrameInfo {
|
|||
fn new(scale: Fraction, a: FlexibleDimension, b: FlexibleDimension) -> Self {
|
||||
let a = match a {
|
||||
FlexibleDimension::Auto => None,
|
||||
FlexibleDimension::Dimension(dimension) => {
|
||||
Some(dimension.into_px(scale).into_unsigned())
|
||||
}
|
||||
FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)),
|
||||
};
|
||||
let b = match b {
|
||||
FlexibleDimension::Auto => None,
|
||||
FlexibleDimension::Dimension(dimension) => {
|
||||
Some(dimension.into_px(scale).into_unsigned())
|
||||
}
|
||||
FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)),
|
||||
};
|
||||
Self { a, b }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,19 @@ use std::time::Duration;
|
|||
|
||||
use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::StrokeOptions;
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn};
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::components::{
|
||||
AutoFocusableControls, Easing, IntrinsicPadding, OpaqueWidgetColor, SurfaceColor, TextColor,
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetCacheKey, WidgetContext,
|
||||
};
|
||||
use crate::styles::Styles;
|
||||
use crate::styles::components::{
|
||||
AutoFocusableControls, Easing, HighlightColor, IntrinsicPadding, OpaqueWidgetColor,
|
||||
OutlineColor, SurfaceColor, TextColor,
|
||||
};
|
||||
use crate::styles::{ColorExt, Styles};
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED};
|
||||
|
|
@ -25,19 +28,100 @@ pub struct Button {
|
|||
pub content: WidgetRef,
|
||||
/// The callback that is invoked when the button is clicked.
|
||||
pub on_click: Option<Callback<()>>,
|
||||
/// The enabled state of the button.
|
||||
pub enabled: Value<bool>,
|
||||
currently_enabled: bool,
|
||||
/// The kind of button to draw.
|
||||
pub kind: Value<ButtonKind>,
|
||||
buttons_pressed: usize,
|
||||
active_style: Option<Dynamic<ButtonStyle>>,
|
||||
cached_state: CacheState,
|
||||
active_colors: Option<Dynamic<ButtonColors>>,
|
||||
color_animation: AnimationHandle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
struct CacheState {
|
||||
key: WidgetCacheKey,
|
||||
kind: ButtonKind,
|
||||
}
|
||||
|
||||
/// The type of a [`Button`] or similar clickable widget.
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum ButtonKind {
|
||||
/// A solid button.
|
||||
#[default]
|
||||
Solid,
|
||||
/// An outline button, which uses the same colors as [`ButtonKind::Solid`]
|
||||
/// but swaps the outline and background colors.
|
||||
Outline,
|
||||
/// A transparent button, which is transparent until it is hovered.
|
||||
Transparent,
|
||||
}
|
||||
|
||||
impl ButtonKind {
|
||||
/// Returns the [`ButtonColors`] to apply for a
|
||||
/// [default](MakeWidget::into_default) button.
|
||||
#[must_use]
|
||||
pub fn colors_for_default(
|
||||
self,
|
||||
visual_state: VisualState,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> ButtonColors {
|
||||
match self {
|
||||
ButtonKind::Solid => match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.theme().primary.color,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.theme().primary.color_bright,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.theme().primary.color_dim,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.theme().primary.color_dim,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
},
|
||||
ButtonKind::Outline | ButtonKind::Transparent => match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.get(&ButtonOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().primary.color,
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&ButtonHoverOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().primary.color_bright,
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveOutline),
|
||||
foreground: context.theme().primary.color,
|
||||
outline: context.theme().surface.color,
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.get(&ButtonDisabledOutline),
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: context.theme().primary.color_dim,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The coloring to apply to a [`Button`] or button-like widget.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, LinearInterpolate)]
|
||||
struct ButtonStyle {
|
||||
background: Color,
|
||||
foreground: Color,
|
||||
outline: Color,
|
||||
pub struct ButtonColors {
|
||||
/// The background color of the button.
|
||||
pub background: Color,
|
||||
/// The foreground (text) color of the button.
|
||||
pub foreground: Color,
|
||||
/// A color to use to surround the button.
|
||||
pub outline: Color,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
|
|
@ -46,14 +130,24 @@ impl Button {
|
|||
Self {
|
||||
content: content.widget_ref(),
|
||||
on_click: None,
|
||||
enabled: Value::Constant(true),
|
||||
currently_enabled: true,
|
||||
cached_state: CacheState {
|
||||
key: WidgetCacheKey::default(),
|
||||
kind: ButtonKind::default(),
|
||||
},
|
||||
buttons_pressed: 0,
|
||||
active_style: None,
|
||||
active_colors: None,
|
||||
kind: Value::Constant(ButtonKind::default()),
|
||||
color_animation: AnimationHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the button's `kind` and returns self.
|
||||
#[must_use]
|
||||
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
|
||||
self.kind = kind.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `on_click` callback and returns self.
|
||||
///
|
||||
/// This callback will be invoked each time the button is clicked.
|
||||
|
|
@ -66,53 +160,80 @@ impl Button {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the value to use for the button's enabled status.
|
||||
#[must_use]
|
||||
pub fn enabled(mut self, enabled: impl IntoValue<bool>) -> Self {
|
||||
self.enabled = enabled.into_value();
|
||||
self.currently_enabled = self.enabled.get();
|
||||
self
|
||||
}
|
||||
|
||||
fn invoke_on_click(&mut self) {
|
||||
if self.enabled.get() {
|
||||
fn invoke_on_click(&mut self, context: &WidgetContext<'_, '_>) {
|
||||
if context.enabled() {
|
||||
if let Some(on_click) = self.on_click.as_mut() {
|
||||
on_click.invoke(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) {
|
||||
let new_style = match () {
|
||||
() if !self.enabled.get() => ButtonStyle {
|
||||
background: context.get(&ButtonDisabledBackground),
|
||||
foreground: context.get(&ButtonDisabledForeground),
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
fn visual_style(context: &WidgetContext<'_, '_>) -> VisualState {
|
||||
if !context.enabled() {
|
||||
VisualState::Disabled
|
||||
} else if context.active() {
|
||||
VisualState::Active
|
||||
} else if context.hovered() {
|
||||
VisualState::Hovered
|
||||
} else {
|
||||
VisualState::Normal
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the coloring to apply to a [`ButtonKind::Transparent`] button.
|
||||
#[must_use]
|
||||
pub fn colors_for_transparent(
|
||||
visual_state: VisualState,
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> ButtonColors {
|
||||
match visual_state {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: Color::CLEAR_BLACK,
|
||||
foreground: context.get(&TextColor),
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
// TODO this probably should use actual style.
|
||||
() if context.is_default() => ButtonStyle {
|
||||
background: context.theme().primary.color,
|
||||
foreground: context.theme().primary.on_color,
|
||||
outline: Color::CLEAR_BLACK,
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&OpaqueWidgetColor),
|
||||
foreground: context.get(&TextColor),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
() if context.active() => ButtonStyle {
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveBackground),
|
||||
foreground: context.get(&ButtonActiveForeground),
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
() if context.hovered() => ButtonStyle {
|
||||
background: context.get(&ButtonHoverBackground),
|
||||
foreground: context.get(&ButtonHoverForeground),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
() => ButtonStyle {
|
||||
background: context.get(&ButtonBackground),
|
||||
foreground: context.get(&ButtonForeground),
|
||||
outline: context.get(&ButtonOutline),
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: Color::CLEAR_BLACK,
|
||||
foreground: context.theme().surface.on_color_variant,
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors {
|
||||
let kind = self.kind.get_tracked(context);
|
||||
let visual_state = Self::visual_style(context);
|
||||
|
||||
self.cached_state = CacheState {
|
||||
key: context.cache_key(),
|
||||
kind,
|
||||
};
|
||||
|
||||
match (immediate, &self.active_style) {
|
||||
if context.is_default() {
|
||||
kind.colors_for_default(visual_state, context)
|
||||
} else {
|
||||
match kind {
|
||||
ButtonKind::Transparent => Self::colors_for_transparent(visual_state, context),
|
||||
ButtonKind::Solid => visual_state.solid_colors(context),
|
||||
ButtonKind::Outline => visual_state.outline_colors(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) {
|
||||
let new_style = self.determine_stateful_colors(context);
|
||||
|
||||
match (immediate, &self.active_colors) {
|
||||
(false, Some(style)) => {
|
||||
self.color_animation = (style.transition_to(new_style))
|
||||
.over(Duration::from_millis(150))
|
||||
|
|
@ -126,43 +247,136 @@ impl Button {
|
|||
_ => {
|
||||
let new_style = Dynamic::new(new_style);
|
||||
let foreground = new_style.map_each(|s| s.foreground);
|
||||
self.active_style = Some(new_style);
|
||||
self.active_colors = Some(new_style);
|
||||
context.attach_styles(Styles::new().with(&TextColor, foreground));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonStyle {
|
||||
if self.active_style.is_none() {
|
||||
fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors {
|
||||
if self.active_colors.is_none() {
|
||||
self.update_colors(context, false);
|
||||
}
|
||||
|
||||
let style = self.active_style.as_ref().expect("always initialized");
|
||||
let style = self.active_colors.as_ref().expect("always initialized");
|
||||
context.redraw_when_changed(style);
|
||||
style.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// The effective visual state of an element.
|
||||
///
|
||||
/// While an element may be multiple states (e.g., active and hovered), when
|
||||
/// rendering a widget sometimes a single visual style must take priority. This
|
||||
/// enum represents the various states a widget may be in for such a decision.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum VisualState {
|
||||
/// The widget should render in its normal state.
|
||||
Normal,
|
||||
/// The widget should render in reaction to the mouse cursor being above the
|
||||
/// widget.
|
||||
Hovered,
|
||||
/// The widget should render in reaction to the widget being clicked on or
|
||||
/// activated by the user.
|
||||
Active,
|
||||
/// The widget should render in a way to convey to the user it is not
|
||||
/// enabled for interaction.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl VisualState {
|
||||
/// Returns the colors to apply to a [`ButtonKind::Solid`] [`Button`] or
|
||||
/// button-like widget.
|
||||
#[must_use]
|
||||
pub fn solid_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
match self {
|
||||
VisualState::Normal => ButtonColors {
|
||||
background: context.get(&ButtonBackground),
|
||||
foreground: context.get(&ButtonForeground),
|
||||
outline: context.get(&ButtonOutline),
|
||||
},
|
||||
VisualState::Hovered => ButtonColors {
|
||||
background: context.get(&ButtonHoverBackground),
|
||||
foreground: context.get(&ButtonHoverForeground),
|
||||
outline: context.get(&ButtonHoverOutline),
|
||||
},
|
||||
VisualState::Active => ButtonColors {
|
||||
background: context.get(&ButtonActiveBackground),
|
||||
foreground: context.get(&ButtonActiveForeground),
|
||||
outline: context.get(&ButtonActiveOutline),
|
||||
},
|
||||
VisualState::Disabled => ButtonColors {
|
||||
background: context.get(&ButtonDisabledBackground),
|
||||
foreground: context.get(&ButtonDisabledForeground),
|
||||
outline: context.get(&ButtonDisabledOutline),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the colors to apply to a [`ButtonKind::Outline`] [`Button`] or
|
||||
/// button-like widget.
|
||||
#[must_use]
|
||||
pub fn outline_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors {
|
||||
let solid = self.solid_colors(context);
|
||||
ButtonColors {
|
||||
background: solid.outline,
|
||||
foreground: solid.foreground,
|
||||
outline: solid.background,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
#![allow(clippy::similar_names)]
|
||||
let enabled = self.enabled.get();
|
||||
// TODO This seems ugly. It needs context, so it can't be moved into the
|
||||
// dynamic system.
|
||||
if self.currently_enabled != enabled {
|
||||
self.update_colors(context, false);
|
||||
self.currently_enabled = enabled;
|
||||
}
|
||||
|
||||
self.enabled.redraw_when_changed(context);
|
||||
let current_style = self.kind.get_tracked(context);
|
||||
if self.cached_state.key != context.cache_key() || self.cached_state.kind != current_style {
|
||||
self.update_colors(context, false);
|
||||
}
|
||||
|
||||
let style = self.current_style(context);
|
||||
context.gfx.fill(style.background);
|
||||
|
||||
context.stroke_outline::<Lp>(style.outline, StrokeOptions::default());
|
||||
let two_lp_stroke = StrokeOptions::lp_wide(Lp::points(2));
|
||||
context.stroke_outline(style.outline, two_lp_stroke);
|
||||
|
||||
if context.focused() {
|
||||
context.draw_focus_ring();
|
||||
if current_style == ButtonKind::Transparent {
|
||||
let two_lp_stroke = two_lp_stroke.into_px(context.gfx.scale());
|
||||
let focus_color = context.get(&HighlightColor);
|
||||
// Some states of a transparent button have solid background
|
||||
// colors. most_contrasting from a 0-alpha color is not a
|
||||
// meaningful measurement, so we only start measuring contrast
|
||||
// once we reach 50% opacity. If we ever add solid background
|
||||
// tracking (<https://github.com/khonsulabs/gooey/issues/73>),
|
||||
// we should use that color for most_contrasting always.
|
||||
let color = if style.background.alpha() > 128 {
|
||||
style
|
||||
.background
|
||||
.most_contrasting(&[focus_color, context.get(&TextColor)])
|
||||
} else {
|
||||
focus_color
|
||||
};
|
||||
|
||||
let inset = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
|
||||
let focus_ring = Shape::stroked_rect(
|
||||
Rect::new(
|
||||
Point::new(inset, inset),
|
||||
context.gfx.region().size - inset * 2,
|
||||
),
|
||||
color,
|
||||
two_lp_stroke,
|
||||
);
|
||||
context
|
||||
.gfx
|
||||
.draw_shape(&focus_ring, Point::default(), None, None);
|
||||
} else if context.is_default() {
|
||||
context.stroke_outline(context.get(&OutlineColor), two_lp_stroke);
|
||||
} else {
|
||||
context.draw_focus_ring();
|
||||
}
|
||||
}
|
||||
|
||||
let content = self.content.mounted(&mut context.as_event_context());
|
||||
|
|
@ -174,7 +388,7 @@ impl Widget for Button {
|
|||
}
|
||||
|
||||
fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
self.enabled.get() && context.get(&AutoFocusableControls).is_all()
|
||||
context.get(&AutoFocusableControls).is_all()
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
|
|
@ -226,7 +440,7 @@ impl Widget for Button {
|
|||
{
|
||||
context.focus();
|
||||
|
||||
self.invoke_on_click();
|
||||
self.invoke_on_click(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -237,17 +451,27 @@ impl Widget for Button {
|
|||
available_space: Size<crate::ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
let double_padding = padding * 2;
|
||||
let mounted = self.content.mounted(&mut context.as_event_context());
|
||||
let available_space = Size::new(
|
||||
available_space.width - double_padding,
|
||||
available_space.height - double_padding,
|
||||
);
|
||||
let size = context.for_other(&mounted).layout(available_space);
|
||||
let size = Size::new(
|
||||
available_space
|
||||
.width
|
||||
.fit_measured(size.width, context.gfx.scale()),
|
||||
available_space
|
||||
.height
|
||||
.fit_measured(size.height, context.gfx.scale()),
|
||||
);
|
||||
context.set_child_layout(
|
||||
&mounted,
|
||||
Rect::new(Point::new(padding, padding), size).into_signed(),
|
||||
);
|
||||
size + padding * 2
|
||||
size + double_padding
|
||||
}
|
||||
|
||||
fn keyboard_input(
|
||||
|
|
@ -264,7 +488,7 @@ impl Widget for Button {
|
|||
let changed = context.activate();
|
||||
if !changed {
|
||||
// The widget was already active. This is now a repeated keypress
|
||||
self.invoke_on_click();
|
||||
self.invoke_on_click(context);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
|
@ -299,7 +523,7 @@ impl Widget for Button {
|
|||
// If we have no buttons pressed, the event should fire on activate not
|
||||
// on deactivate.
|
||||
if self.buttons_pressed == 0 {
|
||||
self.invoke_on_click();
|
||||
self.invoke_on_click(context);
|
||||
}
|
||||
self.update_colors(context, true);
|
||||
}
|
||||
|
|
@ -317,7 +541,7 @@ define_components! {
|
|||
ButtonActiveBackground(Color, "active_background_color", .surface.color)
|
||||
/// The background color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonHoverBackground(Color, "hover_background_color", .surface.bright_color)
|
||||
ButtonHoverBackground(Color, "hover_background_color", .surface.lowest_container)
|
||||
/// The background color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonDisabledBackground(Color, "disabled_background_color", .surface.dim_color)
|
||||
|
|
@ -334,12 +558,12 @@ define_components! {
|
|||
/// The outline color of the button.
|
||||
ButtonOutline(Color, "outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when it is active (depressed).
|
||||
ButtonActiveOutline(Color, "active_outline_color", contrasting!(ButtonActiveBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonActiveOutline(Color, "active_outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonHoverOutline(Color, "hover_outline_color", contrasting!(ButtonHoverBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonHoverOutline(Color, "hover_outline_color", Color::CLEAR_BLACK)
|
||||
/// The outline color of the button when the mouse cursor is hovering over
|
||||
/// it.
|
||||
ButtonDisabledOutline(Color, "disabled_outline_color", contrasting!(ButtonDisabledBackground, ButtonOutline, TextColor, SurfaceColor))
|
||||
ButtonDisabledOutline(Color, "disabled_outline_color", Color::CLEAR_BLACK)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
233
src/widgets/checkbox.rs
Normal file
233
src/widgets/checkbox.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//! A tri-state, labelable checkbox widget.
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Not;
|
||||
|
||||
use kludgine::figures::units::{Lp, Px};
|
||||
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::{PathBuilder, Shape, StrokeOptions};
|
||||
|
||||
use crate::context::{GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{
|
||||
IntrinsicPadding, LineHeight, OutlineColor, TextColor, WidgetAccentColor,
|
||||
};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrappedLayout, WrapperWidget};
|
||||
use crate::widgets::button::ButtonKind;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A labeled-widget that supports three states: Checked, Unchecked, and
|
||||
/// Indeterminant
|
||||
pub struct Checkbox {
|
||||
/// The state (value) of the checkbox.
|
||||
pub state: Dynamic<CheckboxState>,
|
||||
/// The button kind to use as the basis for this checkbox. Checkboxes
|
||||
/// default to [`ButtonKind::Transparent`].
|
||||
pub kind: Value<ButtonKind>,
|
||||
label: WidgetInstance,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Returns a new checkbox that updates `state` when clicked. `label` is
|
||||
/// drawn next to the checkbox and is also clickable to toggle the checkbox.
|
||||
///
|
||||
/// `state` can also be a `Dynamic<bool>` if there is no need to represent
|
||||
/// an indeterminant state.
|
||||
pub fn new(state: impl IntoDynamic<CheckboxState>, label: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
state: state.into_dynamic(),
|
||||
kind: Value::Constant(ButtonKind::Transparent),
|
||||
label: label.make_widget(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the button kind to use as the basis for this checkbox, and
|
||||
/// returns self.
|
||||
///
|
||||
/// Checkboxes default to [`ButtonKind::Transparent`].
|
||||
#[must_use]
|
||||
pub fn kind(mut self, kind: impl IntoValue<ButtonKind>) -> Self {
|
||||
self.kind = kind.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MakeWidget for Checkbox {
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
CheckboxLabel {
|
||||
value: self.state.create_reader(),
|
||||
label: WidgetRef::new(self.label),
|
||||
}
|
||||
.into_button()
|
||||
.on_click(move |()| {
|
||||
let mut value = self.state.lock();
|
||||
*value = !*value;
|
||||
})
|
||||
.kind(self.kind)
|
||||
.make_widget()
|
||||
}
|
||||
}
|
||||
|
||||
/// The state/value of a [`Checkbox`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CheckboxState {
|
||||
/// The checkbox should display showing that it is neither checked or
|
||||
/// unchecked.
|
||||
///
|
||||
/// This state is used to represent concepts such as:
|
||||
///
|
||||
/// - States that are neither true/false, or on/off.
|
||||
/// - States that are partially true or partially on.
|
||||
Indeterminant,
|
||||
/// The checkbox should display in an unchecked/off/false state.
|
||||
Unchecked,
|
||||
/// The checkbox should display in an checked/on/true state.
|
||||
Checked,
|
||||
}
|
||||
|
||||
impl From<bool> for CheckboxState {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::Checked
|
||||
} else {
|
||||
Self::Unchecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<CheckboxState> for bool {
|
||||
type Error = CheckboxToBoolError;
|
||||
|
||||
fn try_from(value: CheckboxState) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CheckboxState::Checked => Ok(true),
|
||||
CheckboxState::Unchecked => Ok(false),
|
||||
CheckboxState::Indeterminant => Err(CheckboxToBoolError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for CheckboxState {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
Self::Indeterminant | Self::Unchecked => Self::Checked,
|
||||
Self::Checked => Self::Unchecked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDynamic<CheckboxState> for Dynamic<bool> {
|
||||
fn into_dynamic(self) -> Dynamic<CheckboxState> {
|
||||
self.linked(
|
||||
|bool| CheckboxState::from(*bool),
|
||||
|tri_state: &CheckboxState| bool::try_from(*tri_state).ok(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`CheckboxState::Indeterminant`] was encountered when converting to a
|
||||
/// `bool`.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct CheckboxToBoolError;
|
||||
|
||||
impl Display for CheckboxToBoolError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("CheckboxState was Indeterminant")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CheckboxToBoolError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CheckboxLabel {
|
||||
value: DynamicReader<CheckboxState>,
|
||||
label: WidgetRef,
|
||||
}
|
||||
|
||||
impl WrapperWidget for CheckboxLabel {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.label
|
||||
}
|
||||
|
||||
fn position_child(
|
||||
&mut self,
|
||||
size: Size<Px>,
|
||||
_available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> WrappedLayout {
|
||||
let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale()); // TODO create a component?
|
||||
let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
let label_inset = checkbox_size + padding;
|
||||
let size_with_checkbox = Size::new(size.width + label_inset, size.height).into_unsigned();
|
||||
WrappedLayout {
|
||||
child: Rect::new(Point::new(label_inset, Px(0)), size),
|
||||
size: size_with_checkbox,
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale());
|
||||
let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale());
|
||||
let checkbox_rect = Rect::new(
|
||||
Point::new(padding, padding),
|
||||
Size::new(checkbox_size, checkbox_size),
|
||||
);
|
||||
let stroke_options = StrokeOptions::lp_wide(Lp::points(2)).into_px(context.gfx.scale());
|
||||
match self.value.get_tracking_refresh(context) {
|
||||
state @ (CheckboxState::Checked | CheckboxState::Indeterminant) => {
|
||||
let color = context.get(&WidgetAccentColor);
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(checkbox_rect, color),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale()));
|
||||
let text_color = context.get(&TextColor);
|
||||
let center = icon_area.origin + icon_area.size / 2;
|
||||
if matches!(state, CheckboxState::Checked) {
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width / 4,
|
||||
icon_area.origin.y + icon_area.size.height * 3 / 4,
|
||||
))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width,
|
||||
icon_area.origin.y,
|
||||
))
|
||||
.build()
|
||||
.stroke(text_color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
|
||||
.line_to(Point::new(
|
||||
icon_area.origin.x + icon_area.size.width,
|
||||
center.y,
|
||||
))
|
||||
.build()
|
||||
.stroke(text_color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
CheckboxState::Unchecked => {
|
||||
let color = context.get(&OutlineColor);
|
||||
context.gfx.draw_shape(
|
||||
&Shape::stroked_rect(checkbox_rect, color, stroke_options),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ use kludgine::Color;
|
|||
|
||||
use crate::context::{GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::components::{IntrinsicPadding, SurfaceColor};
|
||||
use crate::styles::{Component, ContainerLevel, Dimension, Edges, Styles};
|
||||
use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles};
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget};
|
||||
use crate::ConstraintLimit;
|
||||
|
|
@ -193,11 +193,7 @@ impl WrapperWidget for Container {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<ConstraintLimit> {
|
||||
let padding_amount = self
|
||||
.padding(context)
|
||||
.size()
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding_amount = self.padding(context).size().into_upx(context.gfx.scale());
|
||||
Size::new(
|
||||
available_space.width - padding_amount.width,
|
||||
available_space.height - padding_amount.height,
|
||||
|
|
@ -250,6 +246,12 @@ impl From<EffectiveBackground> for Component {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequireInvalidation for EffectiveBackground {
|
||||
fn requires_invalidation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
define_components! {
|
||||
Container {
|
||||
/// The container background behind the current widget.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{IntoSigned, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
|
|
@ -133,6 +132,6 @@ impl WrapperWidget for Expand {
|
|||
),
|
||||
};
|
||||
|
||||
Size::<UPx>::new(width, height).into_signed().into()
|
||||
Size::new(width, height).into_signed().into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ impl Input {
|
|||
context.get(&LineHeight).into_px(scale).into_float(),
|
||||
),
|
||||
);
|
||||
self.text.map(|text| {
|
||||
self.text.map_tracking_invalidate(context, |text| {
|
||||
buffer.set_text(
|
||||
kludgine.font_system(),
|
||||
text,
|
||||
|
|
@ -118,6 +118,101 @@ impl Input {
|
|||
editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before)));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(
|
||||
&mut self,
|
||||
input: KeyEvent,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> (bool, EventHandling) {
|
||||
let editor = self.editor_mut(context.kludgine, &context.widget);
|
||||
|
||||
match (input.state, input.logical_key, input.text.as_deref()) {
|
||||
(ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
match key {
|
||||
Key::Backspace => Action::Backspace,
|
||||
Key::Delete => Action::Delete,
|
||||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
(true, HANDLED)
|
||||
}
|
||||
(ElementState::Pressed, key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
|
||||
let modifiers = context.modifiers();
|
||||
match (editor.select_opt(), modifiers.state().shift_key()) {
|
||||
(None, true) => {
|
||||
editor.set_select_opt(Some(editor.cursor()));
|
||||
}
|
||||
(Some(_), false) => {
|
||||
editor.set_select_opt(None);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
match key {
|
||||
Key::ArrowLeft if modifiers.word_select() => Action::PreviousWord,
|
||||
Key::ArrowLeft => Action::Left,
|
||||
Key::ArrowDown => Action::Down,
|
||||
Key::ArrowUp => Action::Up,
|
||||
Key::ArrowRight if modifiers.word_select() => Action::NextWord,
|
||||
Key::ArrowRight => Action::Right,
|
||||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some("a")) if context.modifiers().primary() => {
|
||||
if state.is_pressed() {
|
||||
self.select_all();
|
||||
}
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some("c")) if context.modifiers().primary() => {
|
||||
|
||||
if state.is_pressed() {
|
||||
if let Some(mut clipboard) = context.clipboard_guard() {
|
||||
if let Some(selection) = editor.copy_selection() {
|
||||
match clipboard.set_text(selection) {
|
||||
Ok(()) => {},
|
||||
Err(err) => tracing::error!("error copying to clipboard: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some("v")) if context.modifiers().primary() => {
|
||||
let pasted = state.is_pressed() &&
|
||||
match context.clipboard_guard().map(|mut clipboard| clipboard.get_text()) {
|
||||
Some(Ok(text)) => {
|
||||
editor.insert_string(&text, None);
|
||||
true
|
||||
},
|
||||
None | Some(Err(arboard::Error::ConversionFailure)) => false,
|
||||
Some(Err(err)) => {tracing::error!("error retrieving clipboard contents: {err}"); false},
|
||||
}
|
||||
|
||||
;
|
||||
(pasted, HANDLED)
|
||||
}
|
||||
(state, _, Some(text))
|
||||
if !context.modifiers().primary()
|
||||
&& text != "\t" // tab
|
||||
&& text != "\r" // enter/return
|
||||
&& text != "\u{1b}" // escape
|
||||
=>
|
||||
{
|
||||
if state.is_pressed() {
|
||||
editor.insert_string(text, None);
|
||||
}
|
||||
(state.is_pressed(), HANDLED)
|
||||
}
|
||||
(_, _, _) => (false, IGNORED),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Input {
|
||||
|
|
@ -351,10 +446,7 @@ impl Widget for Input {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
if self.needs_to_select_all {
|
||||
self.needs_to_select_all = false;
|
||||
self.select_all();
|
||||
|
|
@ -392,70 +484,11 @@ impl Widget for Input {
|
|||
on_key.invoke(input.clone())?;
|
||||
}
|
||||
|
||||
let editor = self.editor_mut(context.kludgine, &context.widget);
|
||||
|
||||
// println!(
|
||||
// "Keyboard input: {:?}. {:?}, {:?}",
|
||||
// input.logical_key, input.text, input.physical_key
|
||||
// );
|
||||
let (text_changed, handled) = match (input.state, input.logical_key, input.text.as_deref()) {
|
||||
(ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
match key {
|
||||
Key::Backspace => Action::Backspace,
|
||||
Key::Delete => Action::Delete,
|
||||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
(true, HANDLED)
|
||||
}
|
||||
(ElementState::Pressed, key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
|
||||
let modifiers = context.modifiers();
|
||||
match (editor.select_opt(), modifiers.state().shift_key()) {
|
||||
(None, true) => {
|
||||
editor.set_select_opt(Some(editor.cursor()));
|
||||
}
|
||||
(Some(_), false) => {
|
||||
editor.set_select_opt(None);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
match key {
|
||||
Key::ArrowLeft if modifiers.word_select() => Action::PreviousWord,
|
||||
Key::ArrowLeft => Action::Left,
|
||||
Key::ArrowDown => Action::Down,
|
||||
Key::ArrowUp => Action::Up,
|
||||
Key::ArrowRight if modifiers.word_select() => Action::NextWord,
|
||||
Key::ArrowRight => Action::Right,
|
||||
_ => unreachable!("previously matched"),
|
||||
},
|
||||
);
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some("a")) if context.modifiers().primary() => {
|
||||
if state.is_pressed() {
|
||||
self.select_all();
|
||||
}
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some(text))
|
||||
if !context.modifiers().primary()
|
||||
&& text != "\t" // tab
|
||||
&& text != "\r" // enter/return
|
||||
&& text != "\u{1b}" // escape
|
||||
=>
|
||||
{
|
||||
if state.is_pressed() {
|
||||
editor.insert_string(text, None);
|
||||
}
|
||||
(state.is_pressed(), HANDLED)
|
||||
}
|
||||
(_, _, _) => (false, IGNORED),
|
||||
};
|
||||
let (text_changed, handled) = self.handle_key(input, context);
|
||||
|
||||
if handled.is_break() {
|
||||
context.set_needs_redraw();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
//! A read-only text widget.
|
||||
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size};
|
||||
use kludgine::figures::{Point, ScreenScale, Size};
|
||||
use kludgine::text::{MeasuredText, Text, TextOrigin};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::context::{GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{IntrinsicPadding, TextColor};
|
||||
use crate::value::{Dynamic, IntoValue, Value};
|
||||
use crate::value::{Dynamic, Generation, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, Widget, WidgetInstance};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ use crate::ConstraintLimit;
|
|||
pub struct Label {
|
||||
/// The contents of the label.
|
||||
pub text: Value<String>,
|
||||
prepared_text: Option<(MeasuredText<Px>, Px, Color)>,
|
||||
prepared_text: Option<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
|
|
@ -34,29 +34,34 @@ impl Label {
|
|||
color: Color,
|
||||
width: Px,
|
||||
) -> &MeasuredText<Px> {
|
||||
let check_generation = self.text.generation();
|
||||
match &self.prepared_text {
|
||||
Some((_, prepared_width, prepared_color))
|
||||
if *prepared_color == color && *prepared_width == width => {}
|
||||
Some((prepared, prepared_generation, prepared_width, prepared_color))
|
||||
if *prepared_generation == check_generation
|
||||
&& *prepared_color == color
|
||||
&& (*prepared_width == width
|
||||
|| (*prepared_width < width
|
||||
&& prepared.line_height == prepared.size.height)) => {}
|
||||
_ => {
|
||||
let measured = self.text.map(|text| {
|
||||
context
|
||||
.gfx
|
||||
.measure_text(Text::new(text, color).wrap_at(width))
|
||||
});
|
||||
self.prepared_text = Some((measured, width, color));
|
||||
self.prepared_text = Some((measured, check_generation, width, color));
|
||||
}
|
||||
}
|
||||
|
||||
self.prepared_text
|
||||
.as_ref()
|
||||
.map(|(prepared, _, _)| prepared)
|
||||
.map(|(prepared, _, _, _)| prepared)
|
||||
.expect("always initialized")
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Label {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
self.text.redraw_when_changed(context);
|
||||
self.text.invalidate_when_changed(context);
|
||||
|
||||
let size = context.gfx.region().size;
|
||||
let center = Point::from(size) / 2;
|
||||
|
|
@ -74,10 +79,7 @@ impl Widget for Label {
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let padding = context
|
||||
.get(&IntrinsicPadding)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
|
||||
let color = context.get(&TextColor);
|
||||
let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
|
||||
let prepared = self.prepared_text(context, color, width);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ use crate::window::ThemeMode;
|
|||
|
||||
/// A widget that applies a set of [`ThemeMode`] to all contained widgets.
|
||||
#[derive(Debug)]
|
||||
pub struct ModeSwitch {
|
||||
pub struct ThemedMode {
|
||||
mode: Value<ThemeMode>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl ModeSwitch {
|
||||
impl ThemedMode {
|
||||
/// Returns a new widget that applies `mode` to all of its children.
|
||||
pub fn new(mode: impl IntoValue<ThemeMode>, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
|
|
@ -20,7 +20,7 @@ impl ModeSwitch {
|
|||
}
|
||||
}
|
||||
|
||||
impl WrapperWidget for ModeSwitch {
|
||||
impl WrapperWidget for ThemedMode {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.child
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, ScreenScale, Size};
|
||||
use kludgine::figures::{Fraction, IntoSigned, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
use crate::styles::DimensionRange;
|
||||
|
|
@ -48,9 +47,12 @@ impl Resize {
|
|||
|
||||
/// Resizes `self` to `width`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `width` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`](crate::styles::Dimension)
|
||||
/// - [`Px`](crate::kludgine::figures::units::Px)
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
pub fn width(mut self, width: impl Into<DimensionRange>) -> Self {
|
||||
self.width = width.into();
|
||||
|
|
@ -59,9 +61,12 @@ impl Resize {
|
|||
|
||||
/// Resizes `self` to `height`.
|
||||
///
|
||||
/// `width` can be an individual
|
||||
/// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a
|
||||
/// range.
|
||||
/// `height` can be an any of:
|
||||
///
|
||||
/// - [`Dimension`](crate::styles::Dimension)
|
||||
/// - [`Px`](crate::kludgine::figures::units::Px)
|
||||
/// - [`Lp`](crate::kludgine::figures::units::Lp)
|
||||
/// - A range of any fo the above.
|
||||
#[must_use]
|
||||
pub fn height(mut self, height: impl Into<DimensionRange>) -> Self {
|
||||
self.height = height.into();
|
||||
|
|
@ -94,8 +99,8 @@ impl WrapperWidget for Resize {
|
|||
(self.width.exact_dimension(), self.height.exact_dimension())
|
||||
{
|
||||
Size::new(
|
||||
width.into_px(context.gfx.scale()).into_unsigned(),
|
||||
height.into_px(context.gfx.scale()).into_unsigned(),
|
||||
width.into_upx(context.gfx.scale()),
|
||||
height.into_upx(context.gfx.scale()),
|
||||
)
|
||||
} else {
|
||||
let available_space = Size::new(
|
||||
|
|
@ -104,7 +109,7 @@ impl WrapperWidget for Resize {
|
|||
);
|
||||
context.for_other(&child).layout(available_space)
|
||||
};
|
||||
Size::<UPx>::new(
|
||||
Size::new(
|
||||
self.width.clamp(size.width, context.gfx.scale()),
|
||||
self.height.clamp(size.height, context.gfx.scale()),
|
||||
)
|
||||
|
|
@ -121,9 +126,7 @@ fn override_constraint(
|
|||
match constraint {
|
||||
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
|
||||
ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) {
|
||||
(Some(min), Some(max)) if min == max => {
|
||||
ConstraintLimit::Known(min.into_px(scale).into_unsigned())
|
||||
}
|
||||
(Some(min), Some(max)) if min == max => ConstraintLimit::Known(min.into_upx(scale)),
|
||||
_ => ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,8 +172,7 @@ impl Widget for Scroll {
|
|||
let (mut scroll, current_max_scroll) = self.constrain_scroll();
|
||||
|
||||
let control_size =
|
||||
Size::<UPx>::new(available_space.width.max(), available_space.height.max())
|
||||
.into_signed();
|
||||
Size::new(available_space.width.max(), available_space.height.max()).into_signed();
|
||||
let max_extents = Size::new(
|
||||
if self.enabled.x {
|
||||
ConstraintLimit::ClippedAfter((control_size.width).into_unsigned())
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
//! A widget that allows a user to "slide" between values.
|
||||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect,
|
||||
ScreenScale, Size,
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, Point, Ranged, Rect, ScreenScale,
|
||||
Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::{Color, Origin};
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::OpaqueWidgetColor;
|
||||
use crate::styles::components::{OpaqueWidgetColor, WidgetAccentColor};
|
||||
use crate::styles::Dimension;
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED};
|
||||
|
|
@ -176,7 +177,7 @@ where
|
|||
|
||||
let half_knob = knob_size / 2;
|
||||
|
||||
let mut value = self.value.get_tracked(context);
|
||||
let mut value = self.value.get_tracking_refresh(context);
|
||||
let min = self.minimum.get_tracked(context);
|
||||
let mut max = self.maximum.get_tracked(context);
|
||||
|
||||
|
|
@ -222,14 +223,10 @@ where
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
self.knob_size = context
|
||||
.get(&KnobSize)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
self.knob_size = context.get(&KnobSize).into_upx(context.gfx.scale());
|
||||
let minimum_size = context
|
||||
.get(&MinimumSliderSize)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
.into_upx(context.gfx.scale());
|
||||
|
||||
match (available_space.width, available_space.height) {
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => {
|
||||
|
|
@ -320,10 +317,51 @@ define_components! {
|
|||
/// The minimum length of the slidable dimension.
|
||||
MinimumSliderSize(Dimension, "minimum_size", |context| context.get(&KnobSize) * 2)
|
||||
/// The color of the draggable portion of the knob.
|
||||
KnobColor(Color, "knob_color", .primary.color) // TODO make this pull from a component multiple widgets can share
|
||||
KnobColor(Color, "knob_color", @WidgetAccentColor)
|
||||
/// The color of the track that the knob rests on.
|
||||
TrackColor(Color,"track_color", |context| context.get(&KnobColor))
|
||||
/// The color of the track that the knob rests on.
|
||||
InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor))
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be used in a [`Slider`] widget.
|
||||
pub trait Slidable<T>: IntoDynamic<T> + Sized
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
/// Returns a new slider over the full [range](Ranged) of the type.
|
||||
fn slider(self) -> Slider<T>
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
Slider::from_value(self.into_dynamic())
|
||||
}
|
||||
|
||||
/// Returns a new slider using the value of `self`. The slider will be
|
||||
/// limited to values between `min` and `max`.
|
||||
fn slider_between(self, min: impl IntoValue<T>, max: impl IntoValue<T>) -> Slider<T> {
|
||||
Slider::new(self.into_dynamic(), min, max)
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, T> Slidable<U> for T
|
||||
where
|
||||
T: IntoDynamic<U>,
|
||||
U: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,7 +296,8 @@ struct Layout {
|
|||
total_weights: u32,
|
||||
allocated_space: (UPx, Lp),
|
||||
fractional: Vec<(LotId, u8)>,
|
||||
measured: Vec<LotId>,
|
||||
fit_to_content: Vec<LotId>,
|
||||
premeasured: Vec<LotId>,
|
||||
pub orientation: StackDirection,
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +317,8 @@ impl Layout {
|
|||
total_weights: 0,
|
||||
allocated_space: (UPx(0), Lp(0)),
|
||||
fractional: Vec::new(),
|
||||
measured: Vec::new(),
|
||||
fit_to_content: Vec::new(),
|
||||
premeasured: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,20 +333,23 @@ impl Layout {
|
|||
|
||||
match dimension {
|
||||
StackDimension::FitContent => {
|
||||
self.measured.retain(|&measured| measured != id);
|
||||
self.fit_to_content.retain(|&measured| measured != id);
|
||||
}
|
||||
StackDimension::Fractional { weight } => {
|
||||
self.fractional.retain(|(measured, _)| *measured != id);
|
||||
self.total_weights -= u32::from(weight);
|
||||
}
|
||||
StackDimension::Measured { min, .. } => match min {
|
||||
Dimension::Px(pixels) => {
|
||||
self.allocated_space.0 -= pixels.into_unsigned();
|
||||
StackDimension::Measured { min, .. } => {
|
||||
self.premeasured.retain(|&measured| measured != id);
|
||||
match min {
|
||||
Dimension::Px(pixels) => {
|
||||
self.allocated_space.0 -= pixels.into_unsigned();
|
||||
}
|
||||
Dimension::Lp(lp) => {
|
||||
self.allocated_space.1 -= lp;
|
||||
}
|
||||
}
|
||||
Dimension::Lp(lp) => {
|
||||
self.allocated_space.1 -= lp;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
dimension
|
||||
|
|
@ -364,7 +369,7 @@ impl Layout {
|
|||
let id = self.children.insert(index, child);
|
||||
let layout = match child {
|
||||
StackDimension::FitContent => {
|
||||
self.measured.push(id);
|
||||
self.fit_to_content.push(id);
|
||||
UPx(0)
|
||||
}
|
||||
StackDimension::Fractional { weight } => {
|
||||
|
|
@ -373,11 +378,12 @@ impl Layout {
|
|||
UPx(0)
|
||||
}
|
||||
StackDimension::Measured { min, .. } => {
|
||||
self.premeasured.push(id);
|
||||
match min {
|
||||
Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(),
|
||||
Dimension::Lp(size) => self.allocated_space.1 += size,
|
||||
}
|
||||
min.into_px(scale).into_unsigned()
|
||||
min.into_upx(scale)
|
||||
}
|
||||
};
|
||||
self.layouts.insert(
|
||||
|
|
@ -397,23 +403,43 @@ impl Layout {
|
|||
) -> Size<UPx> {
|
||||
let (space_constraint, other_constraint) = self.orientation.split_size(available);
|
||||
let available_space = space_constraint.max();
|
||||
let allocated_space =
|
||||
self.allocated_space.0 + self.allocated_space.1.into_px(scale).into_unsigned();
|
||||
let allocated_space = self.allocated_space.0 + self.allocated_space.1.into_upx(scale);
|
||||
let mut remaining = available_space.saturating_sub(allocated_space);
|
||||
// If our `other_constraint` is not known, we will need to give child
|
||||
// widgets an opportunity to lay themselves out in the full area. This
|
||||
// requires one extra layout call, so we avoid persisting layouts during
|
||||
// the first loop if this is the case.
|
||||
let needs_final_layout = !matches!(other_constraint, ConstraintLimit::Known(_));
|
||||
|
||||
// Measure the children that fit their content
|
||||
for &id in &self.measured {
|
||||
self.other = UPx(0);
|
||||
for &id in &self.fit_to_content {
|
||||
let index = self.children.index_of_id(id).expect("child not found");
|
||||
let (measured, _) = self.orientation.split_size(measure(
|
||||
let (measured, other) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation
|
||||
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
|
||||
false,
|
||||
!needs_final_layout,
|
||||
));
|
||||
self.layouts[index].size = measured;
|
||||
self.other = self.other.max(other);
|
||||
remaining = remaining.saturating_sub(measured);
|
||||
}
|
||||
|
||||
// Measure measure the "other" dimension for children that we know their size already.
|
||||
for &id in &self.premeasured {
|
||||
let index = self.children.index_of_id(id).expect("child not found");
|
||||
let (_, other) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(self.layouts[index].size),
|
||||
other_constraint,
|
||||
),
|
||||
!needs_final_layout,
|
||||
));
|
||||
self.other = self.other.max(other);
|
||||
}
|
||||
|
||||
// Measure the weighted children within the remaining space
|
||||
if self.total_weights > 0 {
|
||||
let space_per_weight = remaining / self.total_weights;
|
||||
|
|
@ -435,24 +461,21 @@ impl Layout {
|
|||
|
||||
self.layouts[index].size = size;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know the constrained sizes, we can measure the children
|
||||
// to get the other measurement using the constrainted measurement.
|
||||
self.other = UPx(0);
|
||||
let mut offset = UPx(0);
|
||||
for index in 0..self.children.len() {
|
||||
self.layouts[index].offset = offset;
|
||||
offset += self.layouts[index].size;
|
||||
let (_, measured) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()),
|
||||
other_constraint,
|
||||
),
|
||||
false,
|
||||
));
|
||||
self.other = self.other.max(measured);
|
||||
// Now that we know the constrained sizes, we can measure the children
|
||||
// to get the other measurement using the constrainted measurement.
|
||||
for (id, _) in &self.fractional {
|
||||
let index = self.children.index_of_id(*id).expect("child not found");
|
||||
let (_, measured) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)),
|
||||
other_constraint,
|
||||
),
|
||||
!needs_final_layout,
|
||||
));
|
||||
self.other = self.other.max(measured);
|
||||
}
|
||||
}
|
||||
|
||||
self.other = match other_constraint {
|
||||
|
|
@ -460,16 +483,21 @@ impl Layout {
|
|||
ConstraintLimit::ClippedAfter(clip_limit) => self.other.min(clip_limit),
|
||||
};
|
||||
|
||||
// Finally layout the widgets with the final constraints
|
||||
// Finally, compute the offsets of all of the widgets.
|
||||
let mut offset = UPx(0);
|
||||
for index in 0..self.children.len() {
|
||||
self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()),
|
||||
ConstraintLimit::Known(self.other),
|
||||
),
|
||||
true,
|
||||
));
|
||||
self.layouts[index].offset = offset;
|
||||
offset += self.layouts[index].size;
|
||||
if needs_final_layout {
|
||||
self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation.make_size(
|
||||
ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)),
|
||||
ConstraintLimit::Known(self.other),
|
||||
),
|
||||
true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.orientation.make_size(offset, self.other)
|
||||
|
|
|
|||
|
|
@ -1,58 +1,49 @@
|
|||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::figures::Size;
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
use crate::value::{Generation, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrapperWidget};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic};
|
||||
use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that switches its contents based on a value of `T`.
|
||||
pub struct Switcher<T> {
|
||||
value: Value<T>,
|
||||
value_generation: Option<Generation>,
|
||||
factory: Box<dyn SwitchMap<T>>,
|
||||
#[derive(Debug)]
|
||||
pub struct Switcher {
|
||||
source: DynamicReader<WidgetInstance>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl<T> Switcher<T> {
|
||||
impl Switcher {
|
||||
/// Returns a new widget that replaces its contents with the results of
|
||||
/// calling `map` each time `source` is updated.
|
||||
///
|
||||
/// This function is equivalent to calling
|
||||
/// `Self::new(source.into_dynamic().map_each(map))`, but this function's
|
||||
/// signature helps the compiler's type inference work correctly. When using
|
||||
/// new directly, the compiler often requires annotating the closure's
|
||||
/// argument type.
|
||||
pub fn mapping<T, F>(source: impl IntoDynamic<T>, mut map: F) -> Self
|
||||
where
|
||||
F: FnMut(&T, &Dynamic<T>) -> WidgetInstance + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let source = source.into_dynamic();
|
||||
|
||||
Self::new(source.clone().map_each(move |value| map(value, &source)))
|
||||
}
|
||||
|
||||
/// Returns a new widget that replaces its contents with the result of
|
||||
/// `widget_factory` each time `value` changes.
|
||||
#[must_use]
|
||||
pub fn new<W, F>(value: impl IntoValue<T>, mut widget_factory: F) -> Self
|
||||
where
|
||||
F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe + 'static,
|
||||
W: MakeWidget,
|
||||
{
|
||||
let value = value.into_value();
|
||||
let value_generation = value.generation();
|
||||
let child = WidgetRef::new(value.map(|value| widget_factory(value)));
|
||||
Self {
|
||||
value,
|
||||
value_generation,
|
||||
factory: Box::new(widget_factory),
|
||||
child,
|
||||
}
|
||||
pub fn new(source: impl IntoDynamic<WidgetInstance>) -> Self {
|
||||
let mut source = source.into_dynamic().into_reader();
|
||||
let child = WidgetRef::new(source.get());
|
||||
Self { source, child }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for Switcher<T>
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Switcher")
|
||||
.field("value", &self.value)
|
||||
.field("child", &self.child)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WrapperWidget for Switcher<T>
|
||||
where
|
||||
T: Debug + Send + UnwindSafe + 'static,
|
||||
{
|
||||
impl WrapperWidget for Switcher {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.child
|
||||
}
|
||||
|
|
@ -63,11 +54,8 @@ where
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<ConstraintLimit> {
|
||||
let current_generation = self.value.generation();
|
||||
if self.value_generation != current_generation {
|
||||
self.value_generation = current_generation;
|
||||
let new_child = WidgetRef::new(self.value.map(|value| self.factory.invoke(value)));
|
||||
let removed = std::mem::replace(&mut self.child, new_child);
|
||||
if self.source.has_updated() {
|
||||
let removed = std::mem::replace(&mut self.child, WidgetRef::new(self.source.get()));
|
||||
if let WidgetRef::Mounted(removed) = removed {
|
||||
context.remove_child(&removed);
|
||||
}
|
||||
|
|
@ -75,17 +63,3 @@ where
|
|||
available_space
|
||||
}
|
||||
}
|
||||
|
||||
trait SwitchMap<T>: UnwindSafe + Send {
|
||||
fn invoke(&mut self, value: &T) -> WidgetInstance;
|
||||
}
|
||||
|
||||
impl<W, T, F> SwitchMap<T> for F
|
||||
where
|
||||
F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe,
|
||||
W: MakeWidget,
|
||||
{
|
||||
fn invoke(&mut self, value: &T) -> WidgetInstance {
|
||||
self(value).make_widget()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
254
src/window.rs
254
src/window.rs
|
|
@ -1,5 +1,4 @@
|
|||
//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop
|
||||
//! window.
|
||||
//! Types for displaying a [`Widget`] inside of a desktop window.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::OsStr;
|
||||
|
|
@ -7,10 +6,11 @@ use std::ops::{Deref, DerefMut, Not};
|
|||
use std::panic::{AssertUnwindSafe, UnwindSafe};
|
||||
use std::path::Path;
|
||||
use std::string::ToString;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use alot::LotId;
|
||||
use arboard::Clipboard;
|
||||
use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use kludgine::app::winit::event::{
|
||||
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
|
|
@ -27,14 +27,13 @@ use tracing::Level;
|
|||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus,
|
||||
AsEventContext, EventContext, Exclusive, GraphicsContext, InvalidationStatus, LayoutContext,
|
||||
WidgetContext,
|
||||
};
|
||||
use crate::graphics::Graphics;
|
||||
use crate::styles::components::LayoutOrder;
|
||||
use crate::styles::ThemePair;
|
||||
use crate::tree::Tree;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::utils::{IgnorePoison, ModifiersExt};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{
|
||||
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
||||
|
|
@ -46,6 +45,7 @@ use crate::{initialize_tracing, ConstraintLimit, Run};
|
|||
/// A currently running Gooey window.
|
||||
pub struct RunningWindow<'window> {
|
||||
window: kludgine::app::Window<'window, WindowCommand>,
|
||||
clipboard: Option<Arc<Mutex<Clipboard>>>,
|
||||
focused: Dynamic<bool>,
|
||||
occluded: Dynamic<bool>,
|
||||
}
|
||||
|
|
@ -53,11 +53,13 @@ pub struct RunningWindow<'window> {
|
|||
impl<'window> RunningWindow<'window> {
|
||||
pub(crate) fn new(
|
||||
window: kludgine::app::Window<'window, WindowCommand>,
|
||||
clipboard: &Option<Arc<Mutex<Clipboard>>>,
|
||||
focused: &Dynamic<bool>,
|
||||
occluded: &Dynamic<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
window,
|
||||
clipboard: clipboard.clone(),
|
||||
focused: focused.clone(),
|
||||
occluded: occluded.clone(),
|
||||
}
|
||||
|
|
@ -76,6 +78,15 @@ impl<'window> RunningWindow<'window> {
|
|||
pub fn occluded(&self) -> &Dynamic<bool> {
|
||||
&self.occluded
|
||||
}
|
||||
|
||||
/// Returns a locked mutex guard to the OS's clipboard, if one was able to be
|
||||
/// initialized when the window opened.
|
||||
#[must_use]
|
||||
pub fn clipboard_guard(&mut self) -> Option<MutexGuard<'_, Clipboard>> {
|
||||
self.clipboard
|
||||
.as_ref()
|
||||
.map(|mutex| mutex.lock().ignore_poison())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'window> Deref for RunningWindow<'window> {
|
||||
|
|
@ -139,7 +150,7 @@ impl Window<WidgetInstance> {
|
|||
///
|
||||
/// `focused` will be initialized with an initial state
|
||||
/// of `false`.
|
||||
pub fn with_focused(mut self, focused: impl IntoDynamic<bool>) -> Self {
|
||||
pub fn focused(mut self, focused: impl IntoDynamic<bool>) -> Self {
|
||||
let focused = focused.into_dynamic();
|
||||
focused.update(false);
|
||||
self.focused = Some(focused);
|
||||
|
|
@ -154,7 +165,7 @@ impl Window<WidgetInstance> {
|
|||
/// visible, this value will contain `true`.
|
||||
///
|
||||
/// `occluded` will be initialized with an initial state of `false`.
|
||||
pub fn with_occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self {
|
||||
pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self {
|
||||
let occluded = occluded.into_dynamic();
|
||||
occluded.update(false);
|
||||
self.occluded = Some(occluded);
|
||||
|
|
@ -174,10 +185,16 @@ impl Window<WidgetInstance> {
|
|||
/// Setting the [`Dynamic`]'s value will also update the window with the new
|
||||
/// mode until a mode change is detected, upon which the new mode will be
|
||||
/// stored.
|
||||
pub fn with_theme_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self {
|
||||
pub fn themed_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self {
|
||||
self.theme_mode = Some(theme_mode.into_value());
|
||||
self
|
||||
}
|
||||
|
||||
/// Applies `theme` to the widgets in this window.
|
||||
pub fn themed(mut self, theme: impl IntoValue<ThemePair>) -> Self {
|
||||
self.theme = theme.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Behavior> Window<Behavior>
|
||||
|
|
@ -274,28 +291,34 @@ struct GooeyWindow<T> {
|
|||
root: ManagedWidget,
|
||||
contents: Drawing,
|
||||
should_close: bool,
|
||||
mouse_state: MouseState,
|
||||
redraw_status: RedrawStatus,
|
||||
cursor: CursorState,
|
||||
mouse_buttons: AHashMap<DeviceId, AHashMap<MouseButton, WidgetId>>,
|
||||
redraw_status: InvalidationStatus,
|
||||
initial_frame: bool,
|
||||
occluded: Dynamic<bool>,
|
||||
focused: Dynamic<bool>,
|
||||
keyboard_activated: Option<ManagedWidget>,
|
||||
keyboard_activated: Option<WidgetId>,
|
||||
min_inner_size: Option<Size<UPx>>,
|
||||
max_inner_size: Option<Size<UPx>>,
|
||||
theme: Option<DynamicReader<ThemePair>>,
|
||||
current_theme: ThemePair,
|
||||
theme_mode: Value<ThemeMode>,
|
||||
transparent: bool,
|
||||
clipboard: Option<Arc<Mutex<Clipboard>>>,
|
||||
}
|
||||
|
||||
impl<T> GooeyWindow<T>
|
||||
where
|
||||
T: WindowBehavior,
|
||||
{
|
||||
fn request_close(&mut self, window: &mut RunningWindow<'_>) -> bool {
|
||||
self.should_close |= self.behavior.close_requested(window);
|
||||
fn request_close(
|
||||
should_close: &mut bool,
|
||||
behavior: &mut T,
|
||||
window: &mut RunningWindow<'_>,
|
||||
) -> bool {
|
||||
*should_close |= behavior.close_requested(window);
|
||||
|
||||
self.should_close
|
||||
*should_close
|
||||
}
|
||||
|
||||
fn keyboard_activate_widget(
|
||||
|
|
@ -307,7 +330,11 @@ where
|
|||
) {
|
||||
if is_pressed {
|
||||
if let Some(default) = widget.and_then(|id| self.root.tree.widget_from_node(id)) {
|
||||
if let Some(previously_active) = self.keyboard_activated.take() {
|
||||
if let Some(previously_active) = self
|
||||
.keyboard_activated
|
||||
.take()
|
||||
.and_then(|id| self.root.tree.widget(id))
|
||||
{
|
||||
EventContext::new(
|
||||
WidgetContext::new(
|
||||
previously_active,
|
||||
|
|
@ -315,6 +342,7 @@ where
|
|||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -327,13 +355,18 @@ where
|
|||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
.activate();
|
||||
self.keyboard_activated = Some(default);
|
||||
self.keyboard_activated = Some(default.id());
|
||||
}
|
||||
} else if let Some(keyboard_activated) = self.keyboard_activated.take() {
|
||||
} else if let Some(keyboard_activated) = self
|
||||
.keyboard_activated
|
||||
.take()
|
||||
.and_then(|id| self.root.tree.widget(id))
|
||||
{
|
||||
EventContext::new(
|
||||
WidgetContext::new(
|
||||
keyboard_activated,
|
||||
|
|
@ -341,6 +374,7 @@ where
|
|||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -377,14 +411,14 @@ where
|
|||
.map_or(Px::MAX, |height| height.into_px(graphics.scale()));
|
||||
|
||||
let new_min_size = (min_width > 0 || min_height > 0)
|
||||
.then_some(Size::<Px>::new(min_width, min_height).into_unsigned());
|
||||
.then_some(Size::new(min_width, min_height).into_unsigned());
|
||||
|
||||
if new_min_size != self.min_inner_size && resizable {
|
||||
window.set_min_inner_size(new_min_size);
|
||||
self.min_inner_size = new_min_size;
|
||||
}
|
||||
let new_max_size = (max_width > 0 || max_height > 0)
|
||||
.then_some(Size::<Px>::new(max_width, max_height).into_unsigned());
|
||||
.then_some(Size::new(max_width, max_height).into_unsigned());
|
||||
|
||||
if new_max_size != self.max_inner_size && resizable {
|
||||
window.set_max_inner_size(new_max_size);
|
||||
|
|
@ -437,6 +471,10 @@ where
|
|||
.take()
|
||||
.expect("theme always present");
|
||||
|
||||
let clipboard = Clipboard::new()
|
||||
.ok()
|
||||
.map(|clipboard| Arc::new(Mutex::new(clipboard)));
|
||||
|
||||
let theme_mode = match context.settings.borrow_mut().theme_mode.take() {
|
||||
Some(Value::Dynamic(dynamic)) => {
|
||||
dynamic.update(window.theme().into());
|
||||
|
|
@ -447,7 +485,7 @@ where
|
|||
};
|
||||
let transparent = context.settings.borrow().transparent;
|
||||
let mut behavior = T::initialize(
|
||||
&mut RunningWindow::new(window, &focused, &occluded),
|
||||
&mut RunningWindow::new(window, &clipboard, &focused, &occluded),
|
||||
context.user,
|
||||
);
|
||||
let root = Tree::default().push_boxed(behavior.make_root(), None);
|
||||
|
|
@ -462,12 +500,12 @@ where
|
|||
root,
|
||||
contents: Drawing::default(),
|
||||
should_close: false,
|
||||
mouse_state: MouseState {
|
||||
cursor: CursorState {
|
||||
location: None,
|
||||
widget: None,
|
||||
devices: AHashMap::default(),
|
||||
},
|
||||
redraw_status: RedrawStatus::default(),
|
||||
mouse_buttons: AHashMap::default(),
|
||||
redraw_status: InvalidationStatus::default(),
|
||||
initial_frame: true,
|
||||
occluded,
|
||||
focused,
|
||||
|
|
@ -478,6 +516,7 @@ where
|
|||
theme,
|
||||
theme_mode,
|
||||
transparent,
|
||||
clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -497,13 +536,15 @@ where
|
|||
|
||||
self.redraw_status.refresh_received();
|
||||
graphics.reset_text_attributes();
|
||||
self.root.tree.reset_render_order();
|
||||
// TODO re-check why we can't add drain without a range to kempt. Or even intoiter.
|
||||
let invalidations = std::mem::take(&mut *self.redraw_status.invalidations());
|
||||
self.root.tree.new_frame(invalidations.iter().copied());
|
||||
|
||||
let resizable = window.winit().is_resizable();
|
||||
let is_expanded = self.constrain_window_resizing(resizable, &window, graphics);
|
||||
|
||||
let graphics = self.contents.new_frame(graphics);
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
let mut context = GraphicsContext {
|
||||
widget: WidgetContext::new(
|
||||
self.root.clone(),
|
||||
|
|
@ -511,6 +552,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
gfx: Exclusive::Owned(Graphics::new(graphics)),
|
||||
};
|
||||
|
|
@ -614,11 +656,11 @@ where
|
|||
window: kludgine::app::Window<'_, WindowCommand>,
|
||||
_kludgine: &mut Kludgine,
|
||||
) -> bool {
|
||||
self.request_close(&mut RunningWindow::new(
|
||||
window,
|
||||
&self.focused,
|
||||
&self.occluded,
|
||||
))
|
||||
Self::request_close(
|
||||
&mut self.should_close,
|
||||
&mut self.behavior,
|
||||
&mut RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded),
|
||||
)
|
||||
}
|
||||
|
||||
// fn power_preference() -> wgpu::PowerPreference {
|
||||
|
|
@ -651,7 +693,13 @@ where
|
|||
|
||||
// fn scale_factor_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
|
||||
|
||||
// fn resized(&mut self, window: kludgine::app::Window<'_, ()>) {}
|
||||
fn resized(
|
||||
&mut self,
|
||||
_window: kludgine::app::Window<'_, WindowCommand>,
|
||||
_kludgine: &mut Kludgine,
|
||||
) {
|
||||
self.root.invalidate();
|
||||
}
|
||||
|
||||
// fn theme_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
|
||||
|
||||
|
|
@ -672,12 +720,10 @@ where
|
|||
is_synthetic: bool,
|
||||
) {
|
||||
let target = self.root.tree.focused_widget().unwrap_or(self.root.node_id);
|
||||
let target = self
|
||||
.root
|
||||
.tree
|
||||
.widget_from_node(target)
|
||||
.expect("missing widget");
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
let Some(target) = self.root.tree.widget_from_node(target) else {
|
||||
return;
|
||||
};
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
let mut target = EventContext::new(
|
||||
WidgetContext::new(
|
||||
target,
|
||||
|
|
@ -685,6 +731,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -698,7 +745,13 @@ where
|
|||
if !handled {
|
||||
match input.logical_key {
|
||||
Key::Character(ch) if ch == "w" && window.modifiers().primary() => {
|
||||
if input.state.is_pressed() && self.request_close(&mut window) {
|
||||
if input.state.is_pressed()
|
||||
&& Self::request_close(
|
||||
&mut self.should_close,
|
||||
&mut self.behavior,
|
||||
&mut window,
|
||||
)
|
||||
{
|
||||
window.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
|
|
@ -719,14 +772,15 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
let mut visual_order = target.get(&LayoutOrder);
|
||||
if reverse {
|
||||
visual_order = visual_order.rev();
|
||||
target.return_focus();
|
||||
} else {
|
||||
target.advance_focus();
|
||||
}
|
||||
target.advance_focus(visual_order);
|
||||
}
|
||||
}
|
||||
Key::Enter => {
|
||||
|
|
@ -778,7 +832,7 @@ where
|
|||
.expect("missing widget")
|
||||
});
|
||||
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
let mut widget = EventContext::new(
|
||||
WidgetContext::new(
|
||||
widget,
|
||||
|
|
@ -786,6 +840,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -813,7 +868,7 @@ where
|
|||
.widget(self.root.id())
|
||||
.expect("missing widget")
|
||||
});
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
let mut target = EventContext::new(
|
||||
WidgetContext::new(
|
||||
widget,
|
||||
|
|
@ -821,6 +876,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -837,10 +893,24 @@ where
|
|||
position: PhysicalPosition<f64>,
|
||||
) {
|
||||
let location = Point::<Px>::from(position);
|
||||
self.mouse_state.location = Some(location);
|
||||
self.cursor.location = Some(location);
|
||||
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
if let Some(state) = self.mouse_state.devices.get(&device_id) {
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
|
||||
EventContext::new(
|
||||
WidgetContext::new(
|
||||
self.root.clone(),
|
||||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
.update_hovered_widget();
|
||||
|
||||
if let Some(state) = self.mouse_buttons.get(&device_id) {
|
||||
// Mouse Drag
|
||||
for (button, handler) in state {
|
||||
let Some(handler) = self.root.tree.widget(*handler) else {
|
||||
|
|
@ -853,44 +923,15 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
let last_rendered_at = context.last_layout().expect("passed hit test");
|
||||
let Some(last_rendered_at) = context.last_layout() else {
|
||||
continue;
|
||||
};
|
||||
context.mouse_drag(location - last_rendered_at.origin, device_id, *button);
|
||||
}
|
||||
} else {
|
||||
// Hover
|
||||
let mut context = EventContext::new(
|
||||
WidgetContext::new(
|
||||
self.root.clone(),
|
||||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
self.mouse_state.widget = None;
|
||||
for widget in self.root.tree.widgets_at_point(location) {
|
||||
let mut widget_context = context.for_other(&widget);
|
||||
let relative = location
|
||||
- widget_context
|
||||
.last_layout()
|
||||
.expect("passed hit test")
|
||||
.origin;
|
||||
|
||||
if widget_context.hit_test(relative) {
|
||||
widget_context.hover(relative);
|
||||
drop(widget_context);
|
||||
self.mouse_state.widget = Some(widget.id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if self.mouse_state.widget.is_none() {
|
||||
context.clear_hover();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -900,8 +941,9 @@ where
|
|||
kludgine: &mut Kludgine,
|
||||
_device_id: DeviceId,
|
||||
) {
|
||||
if self.mouse_state.widget.take().is_some() {
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
if self.cursor.widget.take().is_some() {
|
||||
let mut window =
|
||||
RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
let mut context = EventContext::new(
|
||||
WidgetContext::new(
|
||||
self.root.clone(),
|
||||
|
|
@ -909,6 +951,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -924,7 +967,7 @@ where
|
|||
state: ElementState,
|
||||
button: MouseButton,
|
||||
) {
|
||||
let mut window = RunningWindow::new(window, &self.focused, &self.occluded);
|
||||
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
EventContext::new(
|
||||
|
|
@ -934,6 +977,7 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -941,10 +985,8 @@ where
|
|||
|
||||
if let (ElementState::Pressed, Some(location), Some(hovered)) = (
|
||||
state,
|
||||
&self.mouse_state.location,
|
||||
self.mouse_state
|
||||
.widget
|
||||
.and_then(|id| self.root.tree.widget(id)),
|
||||
self.cursor.location,
|
||||
self.cursor.widget.and_then(|id| self.root.tree.widget(id)),
|
||||
) {
|
||||
if let Some(handler) = recursively_handle_event(
|
||||
&mut EventContext::new(
|
||||
|
|
@ -954,17 +996,19 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
),
|
||||
|context| {
|
||||
let relative =
|
||||
*location - context.last_layout().expect("passed hit test").origin;
|
||||
let Some(layout) = context.last_layout() else {
|
||||
return IGNORED;
|
||||
};
|
||||
let relative = location - layout.origin;
|
||||
context.mouse_down(relative, device_id, button)
|
||||
},
|
||||
) {
|
||||
self.mouse_state
|
||||
.devices
|
||||
self.mouse_buttons
|
||||
.entry(device_id)
|
||||
.or_default()
|
||||
.insert(button, handler.id());
|
||||
|
|
@ -972,18 +1016,19 @@ where
|
|||
}
|
||||
}
|
||||
ElementState::Released => {
|
||||
let Some(device_buttons) = self.mouse_state.devices.get_mut(&device_id) else {
|
||||
let Some(device_buttons) = self.mouse_buttons.get_mut(&device_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(handler) = device_buttons.remove(&button) else {
|
||||
return;
|
||||
};
|
||||
if device_buttons.is_empty() {
|
||||
self.mouse_state.devices.remove(&device_id);
|
||||
self.mouse_buttons.remove(&device_id);
|
||||
}
|
||||
let Some(handler) = self.root.tree.widget(handler) else {
|
||||
return;
|
||||
};
|
||||
let cursor_location = self.cursor.location;
|
||||
let mut context = EventContext::new(
|
||||
WidgetContext::new(
|
||||
handler,
|
||||
|
|
@ -991,12 +1036,13 @@ where
|
|||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
&mut self.cursor,
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
||||
let relative = if let (Some(last_rendered), Some(location)) =
|
||||
(context.last_layout(), self.mouse_state.location)
|
||||
(context.last_layout(), cursor_location)
|
||||
{
|
||||
Some(location - last_rendered.origin)
|
||||
} else {
|
||||
|
|
@ -1045,10 +1091,9 @@ fn recursively_handle_event(
|
|||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseState {
|
||||
location: Option<Point<Px>>,
|
||||
widget: Option<WidgetId>,
|
||||
devices: AHashMap<DeviceId, AHashMap<MouseButton, WidgetId>>,
|
||||
pub(crate) struct CursorState {
|
||||
pub(crate) location: Option<Point<Px>>,
|
||||
pub(crate) widget: Option<WidgetId>,
|
||||
}
|
||||
|
||||
pub(crate) mod sealed {
|
||||
|
|
@ -1072,6 +1117,7 @@ pub(crate) mod sealed {
|
|||
pub transparent: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WindowCommand {
|
||||
Redraw,
|
||||
// RequestClose,
|
||||
|
|
@ -1079,7 +1125,7 @@ pub(crate) mod sealed {
|
|||
}
|
||||
|
||||
/// Controls whether the light or dark theme is applied.
|
||||
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, LinearInterpolate)]
|
||||
pub enum ThemeMode {
|
||||
/// Applies the light theme
|
||||
Light,
|
||||
|
|
@ -1130,16 +1176,6 @@ impl From<ThemeMode> for window::Theme {
|
|||
}
|
||||
}
|
||||
|
||||
impl LinearInterpolate for ThemeMode {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
if percent >= 0.5 {
|
||||
*target
|
||||
} else {
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PercentBetween for ThemeMode {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
if *min == *max || *self == *min {
|
||||
|
|
|
|||
Loading…
Reference in a new issue