diff --git a/Cargo.lock b/Cargo.lock index 7a8b0ff..3a93ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2966,8 +2966,6 @@ dependencies = [ [[package]] name = "gpui-pre" version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f0bc3ebba070f8981b3f8e905a12f47d27794a30d029e59cdb9edf7a4b3360" dependencies = [ "accesskit", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 2580850..8a44ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,4 +67,5 @@ ely-ui = { path = "crates/ely-ui" } all = { level = "warn", priority = -1 } [patch.crates-io] +gpui-pre = { path = "vendor/gpui-pre" } gpui-pre-windows = { path = "vendor/gpui-pre-windows" } diff --git a/vendor/gpui-pre/Cargo.lock b/vendor/gpui-pre/Cargo.lock new file mode 100644 index 0000000..c09b2c7 --- /dev/null +++ b/vendor/gpui-pre/Cargo.lock @@ -0,0 +1,5167 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "c-enum" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "bzip2", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" +dependencies = [ + "bitflags 2.13.1", + "block", + "cfg-if", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctor" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gpui-pre" +version = "0.3.8" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.13.1", + "chrono", + "core-video", + "criterion", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-bench-metrics", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "hdrhistogram", + "heapless", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "num_cpus", + "objc2", + "objc2-core-foundation", + "objc2-core-video", + "objc2-metal", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.4", + "raw-window-handle", + "regex", + "resvg", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.0", + "stacksafe", + "strum", + "taffy", + "thiserror 2.0.17", + "ttf-parser", + "url", + "usvg", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-font-kit", + "zed-scap", +] + +[[package]] +name = "gpui-pre-bench-metrics" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "627ecb377abdf6a0afeb9992e625e68c63458678968a02fb39d4cae700c60b6b" +dependencies = [ + "anyhow", + "criterion", + "libc", + "perf-event2", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c060863025dc3b8e90febad2d1f4ffd23a5d0dcedba39d3f6b7820ea2fdd3a0" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc834f30f5f49791e214f1fde01a009c7de91c9cd0a9f31dabfed130ba39e6cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2935cf82333da3f41774e5757837756408f01356197b41c817ad2666034453" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad26ca759364694cb55487bebff4c1b7dd6accab49990ff3cda6f7a818a9f4" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui-pre-perf" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcb288a506a476b6cbf97e6ec1cc0118db43f0ca91087588afa1c2d8cd4f842" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4042345bb5bd3ecbc5d15b21ed128154f462a992058d7038cd397631f5462338" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4acd73345bc3c09f1b98c1b21eb4459ad8336f89bb2edcef619e11c5a08456e" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.4", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "477529fd5aa90bff969785ee9a3938484d94aa5101cdf7b84719629c0345f98a" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec0ec176d576b82aed716cf47185856b54ea9528caaf9d01787024689bfd824" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1635ae7d0b05400bfa211ddef6c67dc8d9f223c463022c2b3bf2c1c14c6044f" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-util-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01486fce7722e5d7d7e7bb71c719dfdcc6ed917112bdc7c56d0556fdb60d10ab" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui-pre-zlog" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbe5b98630ef4bfed5a7105ec1062e78d571912bc491388f07ea22f2457c18b" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f806400f6bda1fdd9fc627e158c0e44fb0c4b7767582a3621efcf92f4a850db7" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d337c9acf058238e45ce0570d8a5958439242889492f91ae2d8e324211a61ccd" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.0", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "link-section" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "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 = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +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.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "perf-event-data" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" +dependencies = [ + "bitflags 2.13.1", + "c-enum", + "perf-event-open-sys2", +] + +[[package]] +name = "perf-event-open-sys2" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e" +dependencies = [ + "libc", + "memoffset", +] + +[[package]] +name = "perf-event2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" +dependencies = [ + "bitflags 2.13.1", + "c-enum", + "libc", + "memmap2", + "perf-event-data", + "perf-event-open-sys2", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "num-traits", + "proptest-macro", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "serde_core", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "stacksafe" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f9c34983ac74195c710c473db6fdf1085f64a47dbaa0090d1bea03be70da66" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6feeae42a2d6b0dcb8aeb2f08d9e48cdac600239cf8a20fc59f9e252e86bdfe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sval" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" + +[[package]] +name = "sval_buffer" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "taffy" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + +[[package]] +name = "which" +version = "8.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.17", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "xcb" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "core-text", + "dirs", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.6", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/vendor/gpui-pre/Cargo.toml b/vendor/gpui-pre/Cargo.toml new file mode 100644 index 0000000..bd7e923 --- /dev/null +++ b/vendor/gpui-pre/Cargo.toml @@ -0,0 +1,683 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +name = "gpui-pre" +version = "0.3.8" +authors = ["Nathan Sobo "] +build = "build.rs" +publish = true +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Zed's GPU-accelerated UI framework (gpui-pre snapshot of zed@279fe07)" +homepage = "https://gpui.rs" +readme = "README.md" +keywords = [ + "desktop", + "gui", + "immediate", +] +categories = ["gui"] +license = "Apache-2.0" +repository = "https://github.com/zed-industries/zed" +resolver = "2" + +[package.metadata.cargo-shear] +ignored = [ + "bindgen", + "font-kit", + "getrandom", + "objc2", + "objc2-metal", +] +ignored-paths = ["src/_ownership_and_data_flow.rs"] + +[package.metadata.gpui-pre] +zed-crate = "gpui" +zed-version = "0.2.2" +zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b" + +[features] +bench = ["bench-support"] +bench-support = [ + "profiler", + "dep:criterion", + "dep:bench_metrics", +] +default = [ + "font-kit", + "wayland", + "x11", + "windows-manifest", +] +inspector = ["gpui_macros/inspector"] +leak-detection = [] +profiler = ["dep:hdrhistogram"] +screen-capture = ["scap"] +stacker = ["dep:stacksafe"] +test-support = [ + "collections/test-support", + "http_client/test-support", + "wayland", + "x11", + "proptest", +] +wayland = [] +windows-manifest = ["dep:embed-resource"] +x11 = ["scap?/x11"] + +[lib] +name = "gpui" +path = "src/gpui.rs" + +[[example]] +name = "a11y" +path = "examples/a11y.rs" + +[[example]] +name = "active_state_bug" +path = "examples/active_state_bug.rs" + +[[example]] +name = "anchor" +path = "examples/anchor.rs" + +[[example]] +name = "animation" +path = "examples/animation.rs" + +[[example]] +name = "data_table" +path = "examples/data_table.rs" + +[[example]] +name = "drag_drop" +path = "examples/drag_drop.rs" + +[[example]] +name = "focus_visible" +path = "examples/focus_visible.rs" + +[[example]] +name = "gif_viewer" +path = "examples/gif_viewer.rs" + +[[example]] +name = "gradient" +path = "examples/gradient.rs" + +[[example]] +name = "grid_layout" +path = "examples/grid_layout.rs" + +[[example]] +name = "hello_world" +path = "examples/hello_world.rs" + +[[example]] +name = "image" +path = "examples/image/image.rs" + +[[example]] +name = "image_gallery" +path = "examples/image_gallery.rs" + +[[example]] +name = "image_loading" +path = "examples/image_loading.rs" + +[[example]] +name = "input" +path = "examples/input.rs" + +[[example]] +name = "layer_shell" +path = "examples/layer_shell.rs" + +[[example]] +name = "list_example" +path = "examples/list_example.rs" + +[[example]] +name = "mouse_pressure" +path = "examples/mouse_pressure.rs" + +[[example]] +name = "move_entity_between_windows" +path = "examples/move_entity_between_windows.rs" + +[[example]] +name = "on_window_close_quit" +path = "examples/on_window_close_quit.rs" + +[[example]] +name = "opacity" +path = "examples/opacity.rs" + +[[example]] +name = "ownership_post" +path = "examples/ownership_post.rs" + +[[example]] +name = "painting" +path = "examples/painting.rs" + +[[example]] +name = "paths_bench" +path = "examples/paths_bench.rs" + +[[example]] +name = "pattern" +path = "examples/pattern.rs" + +[[example]] +name = "popover" +path = "examples/popover.rs" + +[[example]] +name = "scrollable" +path = "examples/scrollable.rs" + +[[example]] +name = "set_menus" +path = "examples/set_menus.rs" + +[[example]] +name = "shadow" +path = "examples/shadow.rs" + +[[example]] +name = "svg" +path = "examples/svg/svg.rs" + +[[example]] +name = "system_notifications" +path = "examples/system_notifications.rs" + +[[example]] +name = "tab_stop" +path = "examples/tab_stop.rs" + +[[example]] +name = "testing" +path = "examples/testing.rs" + +[[example]] +name = "text" +path = "examples/text.rs" + +[[example]] +name = "text_layout" +path = "examples/text_layout.rs" + +[[example]] +name = "text_wrapper" +path = "examples/text_wrapper.rs" + +[[example]] +name = "toggle_headless" +path = "examples/toggle_headless.rs" + +[[example]] +name = "tree" +path = "examples/tree.rs" + +[[example]] +name = "uniform_list" +path = "examples/uniform_list.rs" + +[[example]] +name = "view_example" +path = "examples/view_example/view_example_main.rs" + +[[example]] +name = "window" +path = "examples/window.rs" + +[[example]] +name = "window_movable" +path = "examples/window_movable.rs" + +[[example]] +name = "window_positioning" +path = "examples/window_positioning.rs" + +[[example]] +name = "window_shadow" +path = "examples/window_shadow.rs" + +[[test]] +name = "action_macros" +path = "tests/action_macros.rs" + +[dependencies.accesskit] +version = "0.24.0" +features = ["enumn"] + +[dependencies.anyhow] +version = "1.0.86" + +[dependencies.async-task] +version = "4.7" + +[dependencies.backtrace] +version = "0.3" + +[dependencies.bench_metrics] +version = "=0.3.8" +optional = true +package = "gpui-pre-bench-metrics" + +[dependencies.bitflags] +version = "2.6.0" + +[dependencies.chrono] +version = "0.4" +features = ["serde"] + +[dependencies.collections] +version = "=0.3.8" +package = "gpui-pre-collections" + +[dependencies.criterion] +version = "0.8.2" +features = ["html_reports"] +optional = true + +[dependencies.ctor] +version = "1.0.12" + +[dependencies.derive_more] +version = "2.1.1" +features = [ + "add", + "add_assign", + "deref", + "deref_mut", + "display", + "from", + "from_str", + "mul", + "mul_assign", + "not", +] + +[dependencies.etagere] +version = "0.2" + +[dependencies.futures] +version = "0.3.34" + +[dependencies.futures-concurrency] +version = "7.7.1" + +[dependencies.gpui_macros] +version = "=0.3.8" +package = "gpui-pre-macros" + +[dependencies.gpui_shared_string] +version = "=0.3.8" +package = "gpui-pre-shared-string" + +[dependencies.gpui_util] +version = "=0.3.8" +package = "gpui-pre-util" + +[dependencies.hdrhistogram] +version = "7" +optional = true +default-features = false + +[dependencies.heapless] +version = "0.9.2" + +[dependencies.http_client] +version = "=0.3.8" +package = "gpui-pre-http-client" + +[dependencies.image] +version = "0.25.1" +features = [ + "bmp", + "dds", + "exr", + "ff", + "gif", + "hdr", + "ico", + "jpeg", + "png", + "pnm", + "qoi", + "rayon", + "tga", + "tiff", + "webp", +] +default-features = false + +[dependencies.inventory] +version = "0.3.19" + +[dependencies.itertools] +version = "0.14.0" + +[dependencies.log] +version = "0.4.16" +features = [ + "kv_unstable_serde", + "serde", +] + +[dependencies.lyon] +version = "1.0" + +[dependencies.num_cpus] +version = "1.13" + +[dependencies.parking] +version = "2.0.0" + +[dependencies.parking_lot] +version = "0.12.1" + +[dependencies.pin-project] +version = "1.1.10" + +[dependencies.pollster] +version = "0.4.0" + +[dependencies.postage] +version = "0.5" +features = ["futures-traits"] + +[dependencies.profiling] +version = "1" + +[dependencies.proptest] +version = "1" +features = ["attr-macro"] +optional = true +package = "proptest" + +[dependencies.rand] +version = "0.9" + +[dependencies.raw-window-handle] +version = "0.6" + +[dependencies.refineable] +version = "=0.3.8" +package = "gpui-pre-refineable" + +[dependencies.regex] +version = "1.5" + +[dependencies.resvg] +version = "0.46.0" +features = [ + "text", + "system-fonts", + "memmap-fonts", + "raster-images", +] +default-features = false + +[dependencies.scheduler] +version = "=0.3.8" +package = "gpui-pre-scheduler" + +[dependencies.schemars] +version = "1.0" +features = ["indexmap2"] + +[dependencies.seahash] +version = "4.1" + +[dependencies.serde] +version = "1.0.221" +features = [ + "derive", + "rc", +] + +[dependencies.serde_json] +version = "1.0.144" +features = [ + "preserve_order", + "raw_value", +] + +[dependencies.slotmap] +version = "1.0.6" + +[dependencies.smallvec] +version = "1.6" +features = [ + "union", + "const_new", +] + +[dependencies.spin] +version = "0.10.0" + +[dependencies.stacksafe] +version = "1.0" +optional = true + +[dependencies.strum] +version = "0.28" +features = ["derive"] + +[dependencies.sum_tree] +version = "=0.3.8" +package = "gpui-pre-sum-tree" + +[dependencies.taffy] +version = "0.13.0" + +[dependencies.thiserror] +version = "2.0.12" + +[dependencies.ttf-parser] +version = "0.25" + +[dependencies.url] +version = "2.2" + +[dependencies.usvg] +version = "0.46.0" +default-features = false + +[dependencies.util_macros] +version = "=0.3.8" +package = "gpui-pre-util-macros" + +[dependencies.uuid] +version = "1.1.2" +features = [ + "v4", + "v5", + "v7", + "serde", +] + +[dependencies.waker-fn] +version = "1.2.0" + +[dependencies.web-time] +version = "1.1.0" + +[build-dependencies.embed-resource] +version = "3.0" +optional = true + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.async-channel] +version = "2.5.0" +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies.libc] +version = "0.2" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies.scap] +version = "0.0.8-zed" +optional = true +default-features = false +package = "zed-scap" + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies.core-video] +version = "0.5.2" +features = ["metal"] + +[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies.async-channel] +version = "2.5.0" + +[target.'cfg(target_family = "wasm")'.dependencies.getrandom] +version = "0.3.4" +features = ["wasm_js"] + +[target.'cfg(target_family = "wasm")'.dependencies.uuid] +version = "1.1.2" +features = [ + "v4", + "v5", + "v7", + "serde", + "js", +] + +[target.'cfg(target_os = "macos")'.dependencies.font-kit] +version = "0.14.1-zed" +optional = true +package = "zed-font-kit" + +[target.'cfg(target_os = "macos")'.dependencies.log] +version = "0.4.16" +features = [ + "kv_unstable_serde", + "serde", +] + +[target.'cfg(target_os = "macos")'.dependencies.objc2] +version = "0.6" +optional = true + +[target.'cfg(target_os = "macos")'.dependencies.objc2-core-foundation] +version = "0.3.2" +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-core-video] +version = "0.3.2" +features = [ + "CVBuffer", + "CVImageBuffer", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-metal] +version = "0.3" +optional = true + +[target.'cfg(target_os = "macos")'.build-dependencies.bindgen] +version = "0.72" + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.62" +features = [ + "Data_Xml_Dom", + "Foundation_Numerics", + "Globalization_DateTimeFormatting", + "Storage_Search", + "Storage_Streams", + "System_Threading", + "UI_Notifications", + "UI_ViewManagement", + "Wdk_System_SystemServices", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Direct3D", + "Win32_Graphics_Direct3D11", + "Win32_Graphics_Direct3D_Fxc", + "Win32_Graphics_DirectComposition", + "Win32_Graphics_DirectWrite", + "Win32_Graphics_DirectManipulation", + "Win32_Graphics_Dwm", + "Win32_Graphics_Dxgi", + "Win32_Graphics_Dxgi_Common", + "Win32_Graphics_Gdi", + "Win32_Graphics_Imaging", + "Win32_Graphics_Hlsl", + "Win32_Networking_WinSock", + "Win32_Security", + "Win32_Security_Credentials", + "Win32_Security_Cryptography", + "Win32_Storage_FileSystem", + "Win32_Storage_Packaging_Appx", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_Console", + "Win32_System_Diagnostics_Debug", + "Win32_System_DataExchange", + "Win32_System_IO", + "Win32_System_JobObjects", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_Ole", + "Win32_System_Performance", + "Win32_System_Pipes", + "Win32_System_Power", + "Win32_System_RestartManager", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_Variant", + "Win32_System_WinRT", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Shell", + "Win32_UI_Shell_Common", + "Win32_UI_Shell_PropertiesSystem", + "Win32_UI_WindowsAndMessaging", + "Win32_Media", + "Win32_Foundation", + "Win32_System_Power", + "Win32_System_RemoteDesktop", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", +] + +[lints.clippy] +dbg_macro = "deny" +declare_interior_mutable_const = "deny" +disallowed_methods = "deny" +large_enum_variant = "allow" +let_underscore_future = "allow" +nonminimal_bool = "allow" +redundant_clone = "deny" +single_range_in_vec_init = "allow" +todo = "deny" +too_many_arguments = "allow" +type_complexity = "allow" + +[lints.clippy.style] +level = "allow" +priority = -1 + +[lints.rust.unexpected_cfgs] +level = "allow" +priority = 0 diff --git a/vendor/gpui-pre/Cargo.toml.orig b/vendor/gpui-pre/Cargo.toml.orig new file mode 100644 index 0000000..2c8eb1f --- /dev/null +++ b/vendor/gpui-pre/Cargo.toml.orig @@ -0,0 +1,247 @@ +[package] +name = "gpui-pre" +version = "0.3.8" +edition = { workspace = true } +authors = ["Nathan Sobo "] +description = "Zed's GPU-accelerated UI framework (gpui-pre snapshot of zed@279fe07)" +repository = "https://github.com/zed-industries/zed" +publish = true +license = "Apache-2.0" +homepage = "https://gpui.rs" +readme = "README.md" +keywords = ["desktop", "gui", "immediate"] +categories = ["gui"] + +[package.metadata.cargo-shear] +ignored = ["bindgen", "font-kit", "getrandom", "objc2", "objc2-metal"] +ignored-paths = ["src/_ownership_and_data_flow.rs"] + +[package.metadata.gpui-pre] +zed-crate = "gpui" +zed-version = "0.2.2" +zed-rev = "279fe070bb389b79652e52065b2f001edcc0b11b" + +[lib] +path = "src/gpui.rs" +name = "gpui" + +[features] +default = ["font-kit", "wayland", "x11", "windows-manifest"] +test-support = [ + "collections/test-support", + "http_client/test-support", + "wayland", + "x11", + "proptest", +] +bench-support = ["profiler", "dep:criterion", "dep:bench_metrics"] +bench = ["bench-support"] +inspector = ["gpui_macros/inspector"] +leak-detection = [] +wayland = [] +x11 = ["scap?/x11"] +screen-capture = ["scap"] +windows-manifest = ["dep:embed-resource"] +profiler = ["dep:hdrhistogram"] +stacker = ["dep:stacksafe"] + +[dependencies] +accesskit = { workspace = true } +anyhow = { workspace = true } +async-task = { workspace = true } +backtrace = { workspace = true } +bitflags = { workspace = true } +collections = { workspace = true } +bench_metrics = { workspace = true, optional = true } +criterion = { workspace = true, optional = true } +ctor = { workspace = true } +derive_more = { workspace = true } +etagere = { workspace = true } +futures = { workspace = true } +futures-concurrency = { workspace = true } +gpui_macros = { workspace = true } +gpui_shared_string = { workspace = true } +http_client = { workspace = true } +image = { workspace = true } +inventory = { workspace = true } +itertools = { workspace = true } +log = { workspace = true } +num_cpus = "1.13" +parking = "2.0.0" +parking_lot = { workspace = true } +postage = { workspace = true } +proptest = { workspace = true, optional = true } +chrono = { workspace = true } +profiling = { workspace = true } +rand = { workspace = true } +raw-window-handle = { workspace = true } +regex = { workspace = true } +refineable = { workspace = true } +scheduler = { workspace = true } +resvg = { workspace = true } +usvg = { workspace = true } +ttf-parser = "0.25" +util_macros = { workspace = true } +schemars = { workspace = true } +seahash = "4.1" +serde = { workspace = true } +serde_json = { workspace = true } +slotmap = { workspace = true } +smallvec = { workspace = true } +stacksafe = { workspace = true, optional = true } +strum = { workspace = true } +sum_tree = { workspace = true } +taffy = "0.13.0" +thiserror = { workspace = true } +gpui_util = { workspace = true } +hdrhistogram = { workspace = true, optional = true } +waker-fn = "1.2.0" +lyon = "1.0" +pin-project = "1.1.10" +spin = "0.10.0" +pollster = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } +web-time = { workspace = true } +heapless = { workspace = true } + +[build-dependencies] +embed-resource = { version = "3.0", optional = true } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +async-channel = { version = "2.5.0", default-features = false } + +[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] +async-channel = { workspace = true } + +[target.'cfg(target_family = "wasm")'.dependencies] +getrandom = { version = "0.3.4", features = ["wasm_js"] } +uuid = { workspace = true, features = ["js"] } + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +core-video = { workspace = true } + +[target.'cfg(target_os = "macos")'.dependencies] +objc2-core-foundation = { workspace = true } +objc2-core-video = { workspace = true } +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", version = "0.14.1-zed", optional = true } +log = { workspace = true } +objc2 = { workspace = true, optional = true } +objc2-metal = { version = "0.3", optional = true } + +[target.'cfg(target_os = "macos")'.build-dependencies] +bindgen = "0.72" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies] +scap = { workspace = true, optional = true } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +libc = { workspace = true } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { workspace = true, features = [ + "Win32_Foundation", + "Win32_System_Power", + "Win32_System_RemoteDesktop", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", +] } + +[lints] +workspace = true + +[[example]] +name = "hello_world" +path = "examples/hello_world.rs" + +[[example]] +name = "move_entity_between_windows" +path = "examples/move_entity_between_windows.rs" + +[[example]] +name = "image" +path = "examples/image/image.rs" + +[[example]] +name = "input" +path = "examples/input.rs" + +[[example]] +name = "on_window_close_quit" +path = "examples/on_window_close_quit.rs" + +[[example]] +name = "window_movable" +path = "examples/window_movable.rs" + +[[example]] +name = "opacity" +path = "examples/opacity.rs" + +[[example]] +name = "pattern" +path = "examples/pattern.rs" + +[[example]] +name = "set_menus" +path = "examples/set_menus.rs" + +[[example]] +name = "system_notifications" +path = "examples/system_notifications.rs" + +[[example]] +name = "shadow" +path = "examples/shadow.rs" + +[[example]] +name = "svg" +path = "examples/svg/svg.rs" + +[[example]] +name = "tab_stop" +path = "examples/tab_stop.rs" + +[[example]] +name = "text" +path = "examples/text.rs" + +[[example]] +name = "text_wrapper" +path = "examples/text_wrapper.rs" + +[[example]] +name = "tree" +path = "examples/tree.rs" + +[[example]] +name = "uniform_list" +path = "examples/uniform_list.rs" + +[[example]] +name = "window_shadow" +path = "examples/window_shadow.rs" + +[[example]] +name = "grid_layout" +path = "examples/grid_layout.rs" + +[[example]] +name = "list_example" +path = "examples/list_example.rs" + +[[example]] +name = "mouse_pressure" +path = "examples/mouse_pressure.rs" + +[[example]] +name = "a11y" +path = "examples/a11y.rs" + +[[example]] +name = "view_example" +path = "examples/view_example/view_example_main.rs" + +[[example]] +name = "toggle_headless" +path = "examples/toggle_headless.rs" diff --git a/vendor/gpui-pre/LICENSE-APACHE b/vendor/gpui-pre/LICENSE-APACHE new file mode 100644 index 0000000..461a0fe --- /dev/null +++ b/vendor/gpui-pre/LICENSE-APACHE @@ -0,0 +1,222 @@ +Copyright 2022 - 2025 Zed Industries, Inc. + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + + http://www.apache.org/licenses/LICENSE-2.0 + + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + + 1. Definitions. + + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + + END OF TERMS AND CONDITIONS diff --git a/vendor/gpui-pre/README.md b/vendor/gpui-pre/README.md new file mode 100644 index 0000000..132e5ac --- /dev/null +++ b/vendor/gpui-pre/README.md @@ -0,0 +1,98 @@ +# Welcome to GPUI! + +GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +for Rust, designed to support a wide variety of applications. + +## Getting Started + +GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust. Add `gpui`, and optionally `gpui_platform`, to your `Cargo.toml`: + +```toml +gpui = { version = "*" } +gpui_platform = { version = "*", features = ["font-kit", "wayland", "x11"] } +``` + +Everything in a standalone GPUI app starts with an `Application`. You can create one with `gpui_platform::application()`, which picks the windowing and text backends for the host OS, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()` and register your first root view. + +```rust,no_run +use gpui::*; + +fn main() { + gpui_platform::application().run(|cx: &mut App| { + // .. + }); +} +``` + +### `gpui_platform` + +The features on `gpui_platform` are platform-specific, so the list above is a safe cross-platform default. If you build for a single platform, you can trim it: + +- **macOS** — Rendering uses Metal and is always available, but glyph rasterization needs `font-kit`. Without it, GPUI falls back to a placeholder text system that lays text out but renders no glyphs. + + ```toml + gpui_platform = { version = "*", features = ["font-kit"] } + ``` + +- **Linux / FreeBSD** — enable at least one windowing backend for desktop windows: `wayland`, `x11`, or both. These features also compile the renderer and text system, so no separate text feature is needed. + + ```toml + gpui_platform = { version = "*", features = ["wayland", "x11"] } + ``` + +- **Windows** — no features are required. Windowing uses Win32 and text uses DirectWrite. `font-kit` has no effect here. + +### Additional Topics + +- [Ownership and data flow](_ownership_and_data_flow) +- [Accessibility](_accessibility) + +### Dependencies + +GPUI has various system dependencies that it needs in order to work. + +#### macOS + +On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following: + +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. + +> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. + +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) + + ```sh + xcode-select --install + ``` + +- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: + + ```sh + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + ``` + +## The Big Picture + +GPUI offers three different [registers]() depending on your needs: + +- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information. + +- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. + +- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. + +Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. + +## Other Resources + +In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: + +- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. + +- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`. + +- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., + +- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. + +Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/vendor/gpui-pre/build.rs b/vendor/gpui-pre/build.rs new file mode 100644 index 0000000..504a015 --- /dev/null +++ b/vendor/gpui-pre/build.rs @@ -0,0 +1,36 @@ +#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] + +fn main() { + println!("cargo::rustc-check-cfg=cfg(gles)"); + + // Leak detection does bookkeeping on every entity handle, so `test-support` + // does not enable it: that feature must stay cheap enough to compile into + // every build. CI sets `GPUI_LEAK_DETECTION`; the `leak-detection` feature + // turns it on explicitly. + println!("cargo::rustc-check-cfg=cfg(gpui_leak_detection)"); + println!("cargo::rerun-if-env-changed=GPUI_LEAK_DETECTION"); + let requested_by_environment = + std::env::var_os("GPUI_LEAK_DETECTION").is_some_and(|value| !value.is_empty()); + if requested_by_environment || std::env::var_os("CARGO_FEATURE_LEAK_DETECTION").is_some() { + println!("cargo::rustc-cfg=gpui_leak_detection"); + } + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + + if target_os == "windows" { + #[cfg(feature = "windows-manifest")] + embed_resource(); + } +} + +#[cfg(feature = "windows-manifest")] +fn embed_resource() { + let resource_dir = std::path::Path::new("resources/windows"); + let manifest = resource_dir.join("gpui.manifest.xml"); + let rc_file = resource_dir.join("gpui.rc"); + println!("cargo:rerun-if-changed={}", manifest.display()); + println!("cargo:rerun-if-changed={}", rc_file.display()); + embed_resource::compile(rc_file, embed_resource::ParamsIncludeDirs([resource_dir])) + .manifest_required() + .unwrap(); +} diff --git a/vendor/gpui-pre/docs/contexts.md b/vendor/gpui-pre/docs/contexts.md new file mode 100644 index 0000000..9295c77 --- /dev/null +++ b/vendor/gpui-pre/docs/contexts.md @@ -0,0 +1,33 @@ +# Contexts + +GPUI makes extensive use of _context parameters_ (typically named `cx`) to provide access to application state and services. These contexts are references passed to functions, enabling interaction with global state, windows, entities, and system services. + +--- + +## `App` + +The root context granting access to the application's global state. This context owns all entities' data and can be used to read or update the data referenced by an `Entity`. + +## `Context` + +A context provided when interacting with an `Entity`, with additional methods related to that specific entity such as notifying observers and emitting events. This context dereferences into `App`, meaning any function which can take an `App` reference can also take a `Context` reference, allowing you to access the application's global state. + +## `AsyncApp` and `AsyncWindowContext` + +Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with entities with an async context, the calls become fallible, because the context may outlive the window or even the app itself. + +## `TestAppContext` + +These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. + +--- + +# Non-Context Core Types + +## `Window` + +Provides access to the state of an application window. This type has a root view (an `Entity` implementing `Render`) which it can read/update, but since it is not a context, you must pass a `&mut App` (or a context which dereferences to it) to do so, along with other functions interacting with global state. You can obtain a `Window` from an `WindowHandle` by calling `WindowHandle::update`. + +## `Entity` + +A handle to a structure requiring state. This data is owned by the `App` and can be accessed and modified via references to contexts. If `T` implements `Render`, then the entity is sometimes referred to as a view. Entities can be observed by other entities and windows, allowing a closure to be called when `notify` is called on the entity's `Context`. diff --git a/vendor/gpui-pre/docs/key_dispatch.md b/vendor/gpui-pre/docs/key_dispatch.md new file mode 100644 index 0000000..ae7d828 --- /dev/null +++ b/vendor/gpui-pre/docs/key_dispatch.md @@ -0,0 +1,100 @@ +# Key Dispatch + +GPUI is designed for keyboard-first interactivity. + +To expose functionality to the mouse, you render a button with a click handler. + +To expose functionality to the keyboard, you bind an _action_ in a _key context_. + +Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself: + +```rust +mod menu { + #[gpui::action] + struct MoveUp; + + #[gpui::action] + struct MoveDown; +} +``` + +Actions are frequently unit structs, for which we have a macro. The above could also be written: + +```rust +mod menu { + actions!(gpui, [MoveUp, MoveDown]); +} +``` + +Actions can also be more complex types: + +```rust +mod menu { + #[gpui::action] + struct Move { + direction: Direction, + select: bool, + } +} +``` + +To bind actions, chain `on_action` on to your element: + +```rust +impl Render for Menu { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(unimplemented!()) + } +} +``` + +In order to bind keys to actions, you need to declare a _key context_ for part of the element tree by calling `key_context`. + +```rust +impl Render for Menu { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .key_context("menu") + .on_action(|this: &mut Menu, move: &MoveUp, window: &mut Window, cx: &mut Context| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(unimplemented!()) + } +} +``` + +Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name. + +```json +{ + "context": "menu", + "bindings": { + "up": "menu::MoveUp", + "down": "menu::MoveDown" + } +} +``` + +If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name: + +```json +{ + "context": "menu", + "bindings": { + "up": ["menu::Move", {direction: "up", select: false}] + "down": ["menu::Move", {direction: "down", select: false}] + "shift-up": ["menu::Move", {direction: "up", select: true}] + "shift-down": ["menu::Move", {direction: "down", select: true}] + } +} +``` diff --git a/vendor/gpui-pre/examples/README.md b/vendor/gpui-pre/examples/README.md new file mode 100644 index 0000000..395bf5e --- /dev/null +++ b/vendor/gpui-pre/examples/README.md @@ -0,0 +1,157 @@ +# GPUI Examples + +Examples can be run from the Zed repository root: + +```sh +cargo run -p gpui --example hello_world +``` + +## Running in a browser + +A selection of these examples is also served by the standalone web gallery: + +```sh +cd crates/gpui_web/examples/hello_web +trunk serve +``` + +Open to choose an example: + +| Path | Example | +| --- | --- | +| `/hello-world` | Hello world | +| `/text` | Styled text | +| `/text-layout` | Alignment and decorations | +| `/text-wrapper` | Wrapping and truncation | +| `/input` | Text input and selection | +| `/dynamic-fonts` | Download a font on missing-glyph notifications | +| `/prime-sieve` | Background task demo | + +The server supports direct links and reloads at each path. Only the selected +example's WASM module is loaded. Follow **All examples** to return to the gallery; +navigation reloads the page so the previous app and its workers are released. + +Install [Trunk](https://trunk-rs.github.io/trunk/) if needed. The gallery's toolchain file selects +nightly Rust, the WASM target, and `rust-src`. Its Trunk configuration supplies the +cross-origin-isolation headers needed for shared memory and watches the GPUI +example and renderer sources for automatic rebuilds and browser reloads. Use a +browser with WebGPU or WebGL2 support. + +The gallery builds separate Cargo binaries directly from the existing example +sources. To add another browser-compatible example, register its binary in the +gallery's `Cargo.toml`, add an auxiliary Rust asset in `index.html`, and add its +gallery link with the corresponding `data-module` name. + +### Dynamic font loading + +Open `/dynamic-fonts` to see `App::on_missing_glyphs` fetch a versioned +[Noto Sans Devanagari TTF](https://fonts.gstatic.com/s/notosansdevanagari/v30/TuGoUUFzXI5FBtUq5a8bjKYTZjtRU6Sgv3NaV_SNmI0b8QQCQmHn6B2OHjbL_08AlXQly-A.ttf) +from Google Fonts' CDN. The 219 KB font is licensed under the +[SIL Open Font License](https://github.com/google/fonts/blob/main/ofl/notosansdevanagari/OFL.txt). +The CDN permits cross-origin requests and caches the font for one year. +No sample text is sent to the host. + +The example bundles only IBM Plex Sans, downloads the fallback once on demand, +calls `add_fonts`, and refreshes the windows to reshape the text. Emoji continues +to use the default Canvas fallback. The page displays loading and failure states; +use browser network throttling or block `fonts.gstatic.com` and reload to +exercise them. Reload to retry a failed request. + +This is a small script-to-font mapping, not a general font discovery service. +For production, host versioned font assets on your own CDN and choose an explicit +coverage, caching, and retry policy. Use raw TTF/OTF assets rather than passing +Google Fonts CSS or WOFF2 responses directly to `add_fonts`. + +### Canvas font fallback + +The web text system uses browser fonts for eligible missing CJK and emoji +graphemes. It renders each complete grapheme independently, retaining the primary +font's baseline and line-spacing metrics. Contextual CJK spacing and arbitrary +OpenType features are not reproduced. Other scripts remain on Cosmic Text. +Browser font availability is assumed stable for the application's lifetime. + +With the gallery server running, a browser smoke test checks native-font +preservation, whole-grapheme rendering, emoji atlas reuse, and input/deletion: + +```sh +node crates/gpui_web/examples/hello_web/test_canvas_fallback.mjs +``` + +This requires Node.js 22 or newer and Chrome/Chromium. On macOS it uses the +standard Google Chrome application path; elsewhere it uses `chromium`. Set +`CHROME` to override the executable, and pass an optional server URL as the first +argument. Screenshots and logs are saved under the gallery's ignored `target` +directory. The test intercepts clipboard writes rather than modifying the system +clipboard. + +## Where to start + +- `hello_world` shows the basic shape of a GPUI application: create an + `Application`, open a window, create a root view, and render a `div`. +- `input` demonstrates text input, focus, selections, clipboard actions, and + keyboard bindings. +- `uniform_list` shows how to render a simple virtualized list. +- `testing` demonstrates `#[gpui::test]`, `TestAppContext`, actions, focus, and + window-based tests. + +## Layout and styling + +- `grid_layout` demonstrates CSS-grid-style layout. +- `opacity` demonstrates opacity styling. +- `pattern` shows patterned backgrounds. +- `shadow` demonstrates box shadows. +- `text` shows styled text rendering. +- `text_layout` demonstrates text alignment, decoration, weights, and wrapping. +- `text_wrapper` shows wrapping text content. + +## Interaction + +- `anchor` demonstrates anchored positioning. +- `data_table` combines virtualized list rendering with table-style rows and a + custom scrollbar. +- `drag_drop` shows draggable elements and drop targets. +- `focus_visible` demonstrates keyboard-visible focus styling. +- `mouse_pressure` demonstrates pressure-sensitive pointer input where supported. +- `popover` shows floating layers with `deferred` and `anchored`. +- `scrollable` demonstrates scrollable content. +- `tab_stop` shows keyboard tab navigation. + +## Images, drawing, and animation + +- `animation` demonstrates GPUI animations and animated SVG transforms. +- `gif_viewer` shows GIF rendering. +- `gradient` demonstrates linear gradients and color spaces. +- `image` shows local and remote image loading, image sizing, and asset setup. +- `image_gallery` demonstrates image caching and loading remote images. +- `image_loading` shows image loading states and asset loading. +- `painting` demonstrates custom drawing with paths and canvas. +- `svg` shows SVG rendering. + +## Windows and application behavior + +- `move_entity_between_windows` shows moving an entity between windows. +- `on_window_close_quit` demonstrates quitting when a window closes. +- `set_menus` shows application menu setup. +- `system_notifications` demonstrates posting, replacing, dismissing, and responding to operating-system notifications. +- `window` demonstrates creating normal, dialog, popup, and floating windows. +- `window_positioning` demonstrates window bounds and placement. +- `window_shadow` demonstrates window shadow styling. + +## Specialized examples + +These examples are useful when working on GPUI itself, but they may not be the +best starting point for new applications: + +- `active_state_bug` is a focused active-state reproduction. +- `toggle_headless` starts without a display and preserves its app and todo + entities while attaching to or detaching from Wayland or X11. Run it on Linux + with `cargo run -p gpui --example toggle_headless`; its stdin commands are + `ls`, `create [message]`, `open`, `close`, and `quit`. `open` attaches to the + display server in the process environment and opens the window; while headless, + `open DISPLAY=:1` (or `WAYLAND_DISPLAY=… XDG_RUNTIME_DIR=…`) targets another one. + `close` closes the window and returns to headless mode. +- `layer_shell` demonstrates Linux layer-shell windows. +- `list_example` demonstrates bottom-aligned list state and scrollbar behavior. +- `ownership_post` supports the ownership and data-flow documentation. +- `paths_bench` is a path rendering benchmark. +- `tree` renders a deep tree of nested elements. diff --git a/vendor/gpui-pre/examples/a11y.rs b/vendor/gpui-pre/examples/a11y.rs new file mode 100644 index 0000000..ff389f3 --- /dev/null +++ b/vendor/gpui-pre/examples/a11y.rs @@ -0,0 +1,272 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +//! Accessibility (AccessKit) demo app. +//! +//! Run with: `cargo run -p gpui --example a11y` +//! +//! Or on Linux: `cargo run -p gpui --features gpui_platform/wayland,gpui_platform/x11 --example a11y` +//! +//! This app uses GPUI's accessibility APIs to attach structured information to +//! the element tree, which allows assistive technology to see and interact with +//! the UI programmatically. +//! +//! The app behaves as follows: +//! - It opens a single window. +//! - The window's title is "GPUI Accessibility Demo". +//! - The window has a sequence of UI elements, stacked vertically: +//! - A heading with the text "Accessibility Demo". +//! - A row containing two elements: +//! - A spin button (role `SpinButton`) labelled "Counter: ", where +//! `` is the current count. It supports `Increment` and `Decrement` +//! accessible actions, and also increments on click. The numeric value +//! is clamped to a minimum of 0. +//! - A button labelled "Reset counter" that resets the count to 0. +//! - A row containing two elements: +//! - A switch, that can be toggled, and starts disabled. Toggling the switch +//! does nothing. +//! - The text "Enable feature". +//! - A "to-do" list, with three items, each represented with a `Text` element: +//! - "1. Write code" +//! - "2. Run tests" +//! - "3. Ship it" + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + AccessibleAction, App, Bounds, Context, FocusHandle, KeyBinding, Role, SharedString, Toggled, + Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, text, +}; +use gpui_platform::application; + +actions!(a11y_example, [Tab, TabPrev]); + +struct A11yDemo { + focus_handle: FocusHandle, + count: i32, + enabled: bool, +} + +impl A11yDemo { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle, cx); + Self { + focus_handle, + count: 0, + enabled: false, + } + } +} + +impl Render for A11yDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .id("root") + .role(Role::Application) + .aria_label("Accessibility Demo") + .track_focus(&self.focus_handle) + .on_action(cx.listener(|_, _: &Tab, window, cx| window.focus_next(cx))) + .on_action(cx.listener(|_, _: &TabPrev, window, cx| window.focus_prev(cx))) + .size_full() + .flex() + .flex_col() + .gap_4() + .p_4() + .bg(rgb(0x1e1e2e)) + .text_color(rgb(0xcdd6f4)) + // Heading + .child( + div() + .id("heading") + .role(Role::Heading) + .aria_level(1) + .aria_label("Accessibility Demo") + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .child(text!("Accessibility Demo")), + ) + // Counter — uses a SpinButton role with Increment/Decrement + // actions so screen readers can adjust the value directly. + // Click also works via the built-in handler. + .child( + div() + .flex() + .items_center() + .gap_3() + .child( + div() + .id("counter") + .focusable() + .tab_stop(true) + .role(Role::SpinButton) + .aria_label(SharedString::from(format!("Counter: {}", self.count))) + .aria_numeric_value(self.count as f64) + .aria_min_numeric_value(0.0) + .on_a11y_action(AccessibleAction::Increment, { + let this = cx.entity().downgrade(); + move |_, _, cx| { + this.update(cx, |this, cx| { + this.count += 1; + cx.notify(); + }) + .ok(); + } + }) + .on_a11y_action(AccessibleAction::Decrement, { + let this = cx.entity().downgrade(); + move |_, _, cx| { + this.update(cx, |this, cx| { + this.count = (this.count - 1).max(0); + cx.notify(); + }) + .ok(); + } + }) + .on_click(cx.listener(|this, _, _, cx| { + this.count += 1; + cx.notify(); + })) + .px_3() + .py_1() + .rounded_md() + .bg(rgb(0x89b4fa)) + .text_color(rgb(0x1e1e2e)) + .cursor_pointer() + .child(text!(format!("Count: {}", self.count))), + ) + .child( + div() + .id("reset") + .focusable() + .tab_stop(true) + .role(Role::Button) + .aria_label("Reset counter") + .px_3() + .py_1() + .rounded_md() + .bg(rgb(0x585b70)) + .cursor_pointer() + .on_click(cx.listener(|this, _, _, cx| { + this.count = 0; + cx.notify(); + })) + .child(text!("Reset")), + ), + ) + // A toggle switch + .child( + div() + .flex() + .items_center() + .gap_2() + .child( + div() + .id("toggle") + .focusable() + .tab_stop(true) + .role(Role::Switch) + .aria_label("Enable feature") + .aria_toggled(if self.enabled { + Toggled::True + } else { + Toggled::False + }) + .w(px(44.)) + .h(px(24.)) + .rounded_full() + .cursor_pointer() + .when(self.enabled, |el| el.bg(rgb(0x89b4fa))) + .when(!self.enabled, |el| el.bg(rgb(0x585b70))) + .child( + div() + .size(px(20.)) + .rounded_full() + .bg(gpui::white()) + .mt(px(2.)) + .when(self.enabled, |el| el.ml(px(22.))) + .when(!self.enabled, |el| el.ml(px(2.))), + ) + .on_click(cx.listener(|this, _, _, cx| { + this.enabled = !this.enabled; + cx.notify(); + })), + ) + .child(text!("Enable feature")), + ) + // A short list + .child( + div() + .id("task-list") + .role(Role::List) + .aria_label("Tasks") + .flex() + .flex_col() + .gap_1() + .children( + ["Write code", "Run tests", "Ship it"] + .iter() + .enumerate() + .map(|(i, label)| { + div() + .id(("task", i)) + .role(Role::ListItem) + .aria_label(SharedString::from(*label)) + .aria_position_in_set(i + 1) + .aria_size_of_set(3) + .py_1() + .px_2() + // Note: even though this `text!` macro + // produces multiple elements, it doesn't + // need its own unique ID because the parent + // div has different IDs for each string. + .child(text!(format!("{}. {}", i + 1, label))) + }), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.bind_keys([ + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + ]); + + let bounds = Bounds::centered(None, size(px(500.), px(400.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(gpui::TitlebarOptions { + title: Some("GPUI Accessibility Demo".into()), + ..Default::default() + }), + ..Default::default() + }, + |window, cx| cx.new(|cx| A11yDemo::new(window, cx)), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + env_logger::builder() + .filter_level(log::LevelFilter::Warn) + .filter_module("gpui", log::LevelFilter::Info) + .init(); + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/active_state_bug.rs b/vendor/gpui-pre/examples/active_state_bug.rs new file mode 100644 index 0000000..f767ed2 --- /dev/null +++ b/vendor/gpui-pre/examples/active_state_bug.rs @@ -0,0 +1,47 @@ +/// Click the button — the `.active()` background gets stuck on every other click. +use gpui::*; +use gpui_platform::application; + +struct Example; + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + // Colors from Zed's default dark theme + let bg = hsla(215. / 360., 0.12, 0.15, 1.); + let text = hsla(221. / 360., 0.11, 0.86, 1.); + let hover = hsla(225. / 360., 0.118, 0.267, 1.); + let active = hsla(220. / 360., 0.118, 0.20, 1.); + + div().bg(bg).size_full().p_1().child( + div() + .id("button") + .px_2() + .py_0p5() + .rounded_md() + .text_sm() + .text_color(text) + .hover(|s| s.bg(hover)) + .active(|s| s.bg(active)) + .on_click(|_, _, _| {}) + .child("Click me"), + ) + } +} + +fn main() { + application().run(|cx: &mut App| { + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(200.), px(60.)), + cx, + ))), + ..Default::default() + }, + |_, cx| cx.new(|_| Example), + ) + .unwrap(); + cx.activate(true); + }); +} diff --git a/vendor/gpui-pre/examples/anchor.rs b/vendor/gpui-pre/examples/anchor.rs new file mode 100644 index 0000000..b4b280b --- /dev/null +++ b/vendor/gpui-pre/examples/anchor.rs @@ -0,0 +1,207 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + Anchor, AnchoredPositionMode, App, Axis, Bounds, Context, Half as _, InteractiveElement, + ParentElement, Pixels, Point, Render, SharedString, Size, Window, WindowBounds, WindowOptions, + anchored, deferred, div, point, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +struct AnchorDemo { + hovered_button: Option, +} + +struct ButtonDemo { + label: SharedString, + corner: Option, +} + +fn resolved_position(corner: Anchor, button_size: Size) -> Point { + let offset = Point { + x: px(0.), + y: -button_size.height, + }; + + offset + + match corner.other_side_along(Axis::Vertical) { + Anchor::TopLeft => point(px(0.0), px(0.0)), + Anchor::TopCenter => point(button_size.width.half(), px(0.0)), + Anchor::TopRight => point(button_size.width, px(0.0)), + Anchor::LeftCenter => point(button_size.width, button_size.height.half()), + Anchor::RightCenter => point(px(0.), button_size.height.half()), + Anchor::BottomLeft => point(px(0.0), button_size.height), + Anchor::BottomCenter => point(button_size.width / 2.0, button_size.height), + Anchor::BottomRight => point(button_size.width, button_size.height), + } +} + +impl AnchorDemo { + fn buttons() -> Vec { + vec![ + ButtonDemo { + label: "TopLeft".into(), + corner: Some(Anchor::TopLeft), + }, + ButtonDemo { + label: "TopCenter".into(), + corner: Some(Anchor::TopCenter), + }, + ButtonDemo { + label: "TopRight".into(), + corner: Some(Anchor::TopRight), + }, + ButtonDemo { + label: "LeftCenter".into(), + corner: Some(Anchor::LeftCenter), + }, + ButtonDemo { + label: "Center".into(), + corner: None, + }, + ButtonDemo { + label: "RightCenter".into(), + corner: Some(Anchor::RightCenter), + }, + ButtonDemo { + label: "BottomLeft".into(), + corner: Some(Anchor::BottomLeft), + }, + ButtonDemo { + label: "BottomCenter".into(), + corner: Some(Anchor::BottomCenter), + }, + ButtonDemo { + label: "BottomRight".into(), + corner: Some(Anchor::BottomRight), + }, + ] + } +} + +impl Render for AnchorDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let buttons = Self::buttons(); + let button_size = size(px(120.0), px(65.0)); + + div() + .flex() + .flex_col() + .size_full() + .items_center() + .justify_center() + .bg(gpui::white()) + .gap_4() + .p_10() + .child("Popover with Anchor") + .child( + div() + .size_128() + .grid() + .grid_cols(3) + .gap_6() + .relative() + .children(buttons.iter().enumerate().map(|(index, button)| { + let is_hovered = self.hovered_button == Some(index); + let is_hoverable = button.corner.is_some(); + div() + .relative() + .child( + div() + .id(("button", index)) + .w(button_size.width) + .h(button_size.height) + .flex() + .items_center() + .justify_center() + .bg(gpui::white()) + .when(is_hoverable, |this| { + this.border_1() + .rounded_lg() + .border_color(gpui::black()) + .hover(|style| { + style.bg(gpui::black()).text_color(gpui::white()) + }) + .on_hover(cx.listener( + move |this, hovered, _window, cx| { + if *hovered { + this.hovered_button = Some(index); + } else if this.hovered_button == Some(index) { + this.hovered_button = None; + } + cx.notify(); + }, + )) + .child(button.label.clone()) + }), + ) + .when_some(self.hovered_button.filter(|_| is_hovered), |this, index| { + let button = &buttons[index]; + let Some(corner) = button.corner else { + return this; + }; + + let position = resolved_position(corner, button_size); + this.child(deferred( + anchored() + .anchor(corner) + .position(position) + .position_mode(AnchoredPositionMode::Local) + .snap_to_window() + .child( + div() + .py_0p5() + .px_2() + .bg(gpui::black().opacity(0.75)) + .text_color(rgb(0xffffff)) + .rounded_sm() + .shadow_sm() + .min_w(px(100.0)) + .text_sm() + .child(button.label.clone()), + ), + )) + }) + })), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(750.), px(600.)), + cx, + ))), + ..Default::default() + }, + |_, cx| { + cx.new(|_| AnchorDemo { + hovered_button: None, + }) + }, + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/animation.rs b/vendor/gpui-pre/examples/animation.rs new file mode 100644 index 0000000..e055b7d --- /dev/null +++ b/vendor/gpui-pre/examples/animation.rs @@ -0,0 +1,373 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::time::Duration; + +use anyhow::Result; +use gpui::{ + Animation, AnimationExt as _, AnimationPhase, App, AssetSource, Bounds, Context, MouseButton, + MouseDownEvent, MouseMoveEvent, Pixels, SharedString, SpringAnimation, SpringConfig, + Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage, + prelude::*, px, relative, rgba, size, svg, +}; +use gpui_platform::application; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + std::fs::read(path) + .map(Into::into) + .map_err(Into::into) + .map(Some) + } + + fn list(&self, path: &str) -> Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().into_owned(), + )) + }) + .collect::>()) + } +} + +const ARROW_CIRCLE_SVG: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/image/arrow_circle.svg" +); + +struct AnimationExample { + spring_phase: u8, + spring_damping: f32, + spring_damping_drag: Option<(Pixels, f32)>, +} + +impl Render for AnimationExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + const MINIMUM_DAMPING: f32 = 2.0; + const MAXIMUM_DAMPING: f32 = 32.0; + const DAMPING_SLIDER_WIDTH: Pixels = px(224.0); + + let spring_phase = self.spring_phase; + let spring_position = px(98.0 * f32::from(spring_phase)); + let spring_damping = self.spring_damping; + let spring = SpringConfig::new(170.0, spring_damping, 1.0); + let (_, damping_ratio) = spring.canonical(); + let damping_fraction = + (spring_damping - MINIMUM_DAMPING) / (MAXIMUM_DAMPING - MINIMUM_DAMPING); + + div() + .flex() + .flex_col() + .size_full() + .bg(gpui::white()) + .text_color(gpui::black()) + .justify_around() + .child( + div() + .flex() + .flex_col() + .size_full() + .justify_around() + .child( + div() + .id("content") + .flex() + .flex_col() + .h(px(150.)) + .overflow_y_scroll() + .w_full() + .flex_1() + .justify_center() + .items_center() + .text_xl() + .gap_4() + .child("Hello Animation") + .child( + div() + .id("spring-demo") + .flex() + .flex_col() + .gap_2() + .p_2() + .w(px(240.0)) + .rounded_md() + .bg(rgba(0x00000010)) + .cursor_pointer() + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + this.spring_phase = (this.spring_phase + 1) % 3; + cx.notify(); + }), + ) + .child(format!( + "Target phase {spring_phase}: click rapidly to redirect momentum" + )) + .child( + div() + .id("spring-damping-controls") + .flex() + .flex_col() + .gap_1() + .text_sm() + .on_click(|_, _, cx| cx.stop_propagation()) + .child(format!( + "Drag damping: {spring_damping:.1} (ζ {damping_ratio:.2})" + )) + .child( + div() + .id("spring-damping") + .relative() + .h(px(20.0)) + .w(DAMPING_SLIDER_WIDTH) + .cursor_pointer() + .on_mouse_down( + MouseButton::Left, + cx.listener( + |this, + event: &MouseDownEvent, + _, + cx| { + this.spring_damping_drag = Some(( + event.position.x, + this.spring_damping, + )); + cx.stop_propagation(); + }, + ), + ) + .on_mouse_move(cx.listener( + |this, + event: &MouseMoveEvent, + _, + cx| { + let Some(( + start_position, + start_damping, + )) = this.spring_damping_drag + else { + return; + }; + + let delta = (event.position.x + - start_position) + / DAMPING_SLIDER_WIDTH; + this.spring_damping = (start_damping + + delta + * (MAXIMUM_DAMPING + - MINIMUM_DAMPING)) + .clamp( + MINIMUM_DAMPING, + MAXIMUM_DAMPING, + ); + cx.stop_propagation(); + cx.notify(); + }, + )) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + this.spring_damping_drag = None; + cx.stop_propagation(); + }), + ) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + this.spring_damping_drag = None; + cx.stop_propagation(); + }), + ) + .child( + div() + .absolute() + .top(px(8.0)) + .h(px(4.0)) + .w_full() + .rounded_full() + .bg(rgba(0x00000030)), + ) + .child( + div() + .absolute() + .top(px(8.0)) + .h(px(4.0)) + .w(relative(damping_fraction)) + .rounded_full() + .bg(rgba(0x3b82f6ff)), + ) + .child( + div() + .absolute() + .top(px(3.0)) + .left(relative(damping_fraction)) + .ml(px(-7.0)) + .size(px(14.0)) + .rounded_full() + .bg(rgba(0x2563ebff)), + ), + ), + ) + .child( + div().relative().h(px(32.0)).w_full().child( + div() + .absolute() + .top_1() + .size(px(24.0)) + .rounded_full() + .bg(rgba(0x3b82f6ff)) + .cursor_pointer() + .debug_selector(|| "spring-position".into()) + .with_spring( + "spring-position", + SpringAnimation::new(spring) + .to(spring_position) + .with_epsilon(0.25), + |this, left| this.left(left), + ), + ), + ) + .child( + div().h(px(24.0)).rounded_sm().with_spring( + "spring-phase", + SpringAnimation::new(spring) + .to(AnimationPhase(f32::from(spring_phase))) + .with_epsilon(0.001), + |this, phase| { + let (width, color) = if phase.0 <= 1.0 { + ( + phase.interpolate_between( + 0.0..=1.0, + px(48.0), + px(224.0), + ), + phase.interpolate_between_clamped( + 0.0..=1.0, + rgba(0xf97316ff), + rgba(0x22c55eff), + ), + ) + } else { + ( + phase.interpolate_between( + 1.0..=2.0, + px(224.0), + px(96.0), + ), + phase.interpolate_between_clamped( + 1.0..=2.0, + rgba(0x22c55eff), + rgba(0xa855f7ff), + ), + ) + }; + this.w(width).bg(color) + }, + ), + ), + ) + .child( + svg() + .size_20() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(gpui::black()) + .with_animation( + "image_circle", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate( + percentage(delta), + )) + }, + ), + ), + ) + .child( + div() + .flex() + .h(px(64.)) + .w_full() + .p_2() + .justify_center() + .items_center() + .border_t_1() + .border_color(gpui::black().opacity(0.1)) + .bg(gpui::black().opacity(0.05)) + .child("Other Panel"), + ), + ) + } +} + +fn run_example() { + application().with_assets(Assets {}).run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(300.), px(300.)), + cx, + ))), + ..Default::default() + }; + cx.open_window(options, |_, cx| { + cx.activate(false); + cx.new(|_| AnimationExample { + spring_phase: 0, + spring_damping: 14.0, + spring_damping_drag: None, + }) + }) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} + +#[cfg(all(test, feature = "test-support"))] +mod tests { + use gpui::{Modifiers, TestAppContext}; + + use super::*; + + #[gpui::test] + fn clicking_spring_position_changes_the_target(cx: &mut TestAppContext) { + let (view, cx) = cx.add_window_view(|_, _| AnimationExample { + spring_phase: 0, + spring_damping: 14.0, + spring_damping_drag: None, + }); + cx.simulate_resize(size(px(300.0), px(300.0))); + cx.run_until_parked(); + + let position_bounds = cx + .debug_bounds("spring-position") + .expect("spring position should be rendered"); + let position = position_bounds.center(); + cx.simulate_mouse_move(position, None, Modifiers::default()); + cx.simulate_click(position, Modifiers::default()); + cx.run_until_parked(); + + assert_eq!(cx.update(|_, cx| view.read(cx).spring_phase), 1); + } +} diff --git a/vendor/gpui-pre/examples/data_table.rs b/vendor/gpui-pre/examples/data_table.rs new file mode 100644 index 0000000..2bee476 --- /dev/null +++ b/vendor/gpui-pre/examples/data_table.rs @@ -0,0 +1,494 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::{ops::Range, rc::Rc, time::Duration}; + +use gpui::{ + App, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, + SharedString, UniformListScrollHandle, Window, WindowBounds, WindowOptions, canvas, div, point, + prelude::*, px, rgb, size, uniform_list, +}; +use gpui_platform::application; + +const TOTAL_ITEMS: usize = 10000; +const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.); +const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.); + +pub struct Quote { + name: SharedString, + symbol: SharedString, + last_done: f64, + prev_close: f64, + open: f64, + high: f64, + low: f64, + timestamp: Duration, + volume: i64, + turnover: f64, + ttm: f64, + market_cap: f64, + float_cap: f64, + shares: f64, + pb: f64, + pe: f64, + eps: f64, + dividend: f64, + dividend_yield: f64, + dividend_per_share: f64, + dividend_date: SharedString, + dividend_payment: f64, +} + +impl Quote { + pub fn random() -> Self { + use rand::Rng; + let mut rng = rand::rng(); + // simulate a base price in a realistic range + let prev_close = rng.random_range(100.0..200.0); + let change = rng.random_range(-5.0..5.0); + let last_done = prev_close + change; + let open = prev_close + rng.random_range(-3.0..3.0); + let high = (prev_close + rng.random_range::(0.0..10.0)).max(open); + let low = (prev_close - rng.random_range::(0.0..10.0)).min(open); + let timestamp = Duration::from_secs(rng.random_range(0..86400)); + let volume = rng.random_range(1_000_000..100_000_000); + let turnover = last_done * volume as f64; + let symbol = { + let mut ticker = String::new(); + if rng.random_bool(0.5) { + ticker.push_str(&format!( + "{:03}.{}", + rng.random_range(100..1000), + rng.random_range(0..10) + )); + } else { + ticker.push_str(&format!( + "{}{}", + rng.random_range('A'..='Z'), + rng.random_range('A'..='Z') + )); + } + ticker.push_str(&format!(".{}", rng.random_range('A'..='Z'))); + ticker + }; + let name = format!( + "{} {} - #{}", + symbol, + rng.random_range(1..100), + rng.random_range(10000..100000) + ); + let ttm = rng.random_range(0.0..10.0); + let market_cap = rng.random_range(1_000_000.0..10_000_000.0); + let float_cap = market_cap + rng.random_range(1_000.0..10_000.0); + let shares = rng.random_range(100.0..1000.0); + let pb = market_cap / shares; + let pe = market_cap / shares; + let eps = market_cap / shares; + let dividend = rng.random_range(0.0..10.0); + let dividend_yield = rng.random_range(0.0..10.0); + let dividend_per_share = rng.random_range(0.0..10.0); + let dividend_date = SharedString::new(format!( + "{}-{}-{}", + rng.random_range(2000..2023), + rng.random_range(1..12), + rng.random_range(1..28) + )); + let dividend_payment = rng.random_range(0.0..10.0); + + Self { + name: name.into(), + symbol: symbol.into(), + last_done, + prev_close, + open, + high, + low, + timestamp, + volume, + turnover, + pb, + pe, + eps, + ttm, + market_cap, + float_cap, + shares, + dividend, + dividend_yield, + dividend_per_share, + dividend_date, + dividend_payment, + } + } + + fn change(&self) -> f64 { + (self.last_done - self.prev_close) / self.prev_close * 100.0 + } + + fn change_color(&self) -> gpui::Hsla { + if self.change() > 0.0 { + gpui::green() + } else { + gpui::red() + } + } + + fn turnover_ratio(&self) -> f64 { + self.volume as f64 / self.turnover * 100.0 + } +} + +#[derive(IntoElement)] +struct TableRow { + ix: usize, + quote: Rc, +} +impl TableRow { + fn new(ix: usize, quote: Rc) -> Self { + Self { ix, quote } + } + + fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement { + div() + .whitespace_nowrap() + .truncate() + .w(width) + .px_1() + .child(match key { + "id" => div().child(format!("{}", self.ix)), + "symbol" => div().child(self.quote.symbol.clone()), + "name" => div().child(self.quote.name.clone()), + "last_done" => div() + .text_color(color) + .child(format!("{:.3}", self.quote.last_done)), + "prev_close" => div() + .text_color(color) + .child(format!("{:.3}", self.quote.prev_close)), + "change" => div() + .text_color(color) + .child(format!("{:.2}%", self.quote.change())), + "timestamp" => div() + .text_color(color) + .child(format!("{:?}", self.quote.timestamp.as_secs())), + "open" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.open)), + "low" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.low)), + "high" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.high)), + "ttm" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.ttm)), + "eps" => div() + .text_color(color) + .child(format!("{:.2}", self.quote.eps)), + "market_cap" => { + div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0)) + } + "float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)), + "turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)), + "volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)), + "turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())), + "pe" => div().child(format!("{:.2}", self.quote.pe)), + "pb" => div().child(format!("{:.2}", self.quote.pb)), + "shares" => div().child(format!("{:.2}", self.quote.shares)), + "dividend" => div().child(format!("{:.2}", self.quote.dividend)), + "yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)), + "dividend_per_share" => { + div().child(format!("{:.2}", self.quote.dividend_per_share)) + } + "dividend_date" => div().child(format!("{}", self.quote.dividend_date)), + "dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)), + _ => div().child("--"), + }) + } +} + +const FIELDS: [(&str, f32); 24] = [ + ("id", 64.), + ("symbol", 64.), + ("name", 180.), + ("last_done", 80.), + ("prev_close", 80.), + ("open", 80.), + ("low", 80.), + ("high", 80.), + ("ttm", 50.), + ("market_cap", 96.), + ("float_cap", 96.), + ("turnover", 120.), + ("volume", 100.), + ("turnover_ratio", 96.), + ("pe", 64.), + ("pb", 64.), + ("eps", 64.), + ("shares", 96.), + ("dividend", 64.), + ("yield", 64.), + ("dividend_per_share", 64.), + ("dividend_date", 96.), + ("dividend_payment", 64.), + ("timestamp", 120.), +]; + +impl RenderOnce for TableRow { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let color = self.quote.change_color(); + div() + .flex() + .flex_row() + .border_b_1() + .border_color(rgb(0xE0E0E0)) + .bg(if self.ix.is_multiple_of(2) { + rgb(0xFFFFFF) + } else { + rgb(0xFAFAFA) + }) + .py_0p5() + .px_2() + .children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color))) + } +} + +struct DataTable { + /// Use `Rc` to share the same quote data across multiple items, avoid cloning. + quotes: Vec>, + visible_range: Range, + scroll_handle: UniformListScrollHandle, + /// The position in thumb bounds when dragging start mouse down. + drag_position: Option>, +} + +impl DataTable { + fn new() -> Self { + Self { + quotes: Vec::new(), + visible_range: 0..0, + scroll_handle: UniformListScrollHandle::new(), + drag_position: None, + } + } + + fn generate(&mut self) { + self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect(); + } + + fn table_bounds(&self) -> Bounds { + self.scroll_handle.0.borrow().base_handle.bounds() + } + + fn scroll_top(&self) -> Pixels { + self.scroll_handle.0.borrow().base_handle.offset().y + } + + fn scroll_height(&self) -> Pixels { + self.scroll_handle + .0 + .borrow() + .last_item_size + .unwrap_or_default() + .contents + .height + } + + fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let scroll_height = self.scroll_height(); + let table_bounds = self.table_bounds(); + let table_height = table_bounds.size.height; + if table_height == px(0.) { + return div().id("scrollbar"); + } + + let percentage = -self.scroll_top() / scroll_height; + let offset_top = (table_height * percentage).clamp( + px(4.), + (table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)), + ); + let entity = cx.entity(); + let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone(); + + div() + .id("scrollbar") + .absolute() + .top(offset_top) + .right_1() + .h(SCROLLBAR_THUMB_HEIGHT) + .w(SCROLLBAR_THUMB_WIDTH) + .bg(rgb(0xC0C0C0)) + .hover(|this| this.bg(rgb(0xA0A0A0))) + .rounded_lg() + .child( + canvas( + |_, _, _| (), + move |thumb_bounds, _, window, _| { + window.on_mouse_event({ + let entity = entity.clone(); + move |ev: &MouseDownEvent, _, _, cx| { + if !thumb_bounds.contains(&ev.position) { + return; + } + + entity.update(cx, |this, _| { + this.drag_position = Some( + ev.position - thumb_bounds.origin - table_bounds.origin, + ); + }) + } + }); + window.on_mouse_event({ + let entity = entity.clone(); + move |_: &MouseUpEvent, _, _, cx| { + entity.update(cx, |this, _| { + this.drag_position = None; + }) + } + }); + + window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| { + if !ev.dragging() { + return; + } + + let Some(drag_pos) = entity.read(cx).drag_position else { + return; + }; + + let inside_offset = drag_pos.y; + let percentage = ((ev.position.y - table_bounds.origin.y + + inside_offset) + / (table_bounds.size.height)) + .clamp(0., 1.); + + let offset_y = ((scroll_height - table_bounds.size.height) + * percentage) + .clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT); + scroll_handle.set_offset(point(px(0.), -offset_y)); + cx.notify(entity.entity_id()); + }) + }, + ) + .size_full(), + ) + } +} + +impl Render for DataTable { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .bg(gpui::white()) + .text_sm() + .size_full() + .p_4() + .gap_2() + .flex() + .flex_col() + .child(format!( + "Total {} items, visible range: {:?}", + self.quotes.len(), + self.visible_range + )) + .child( + div() + .flex() + .flex_col() + .flex_1() + .overflow_hidden() + .border_1() + .border_color(rgb(0xE0E0E0)) + .rounded_sm() + .child( + div() + .flex() + .flex_row() + .w_full() + .overflow_hidden() + .border_b_1() + .border_color(rgb(0xE0E0E0)) + .text_color(rgb(0x555555)) + .bg(rgb(0xF0F0F0)) + .py_1() + .px_2() + .text_xs() + .children(FIELDS.map(|(key, width)| { + div() + .whitespace_nowrap() + .flex_shrink_0() + .truncate() + .px_1() + .w(px(width)) + .child(key.replace("_", " ").to_uppercase()) + })), + ) + .child( + div() + .relative() + .size_full() + .child( + uniform_list( + "items", + self.quotes.len(), + cx.processor(move |this, range: Range, _, _| { + this.visible_range = range.clone(); + let mut items = Vec::with_capacity(range.end - range.start); + for i in range { + if let Some(quote) = this.quotes.get(i) { + items.push(TableRow::new(i, quote.clone())); + } + } + items + }), + ) + .size_full() + .track_scroll(&self.scroll_handle), + ) + .child(self.render_scrollbar(window, cx)), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window( + WindowOptions { + focus: true, + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(1280.0), px(1000.0)), + cx, + ))), + ..Default::default() + }, + |_, cx| { + cx.new(|_| { + let mut table = DataTable::new(); + table.generate(); + table + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/drag_drop.rs b/vendor/gpui-pre/examples/drag_drop.rs new file mode 100644 index 0000000..8f038ef --- /dev/null +++ b/vendor/gpui-pre/examples/drag_drop.rs @@ -0,0 +1,158 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds, WindowOptions, div, + prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +#[derive(Clone, Copy)] +struct DragInfo { + ix: usize, + color: Hsla, + position: Point, +} + +impl DragInfo { + fn new(ix: usize, color: Hsla) -> Self { + Self { + ix, + color, + position: Point::default(), + } + } + + fn position(mut self, pos: Point) -> Self { + self.position = pos; + self + } +} + +impl Render for DragInfo { + fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { + let size = gpui::size(px(120.), px(50.)); + + div() + .pl(self.position.x - size.width.half()) + .pt(self.position.y - size.height.half()) + .child( + div() + .flex() + .justify_center() + .items_center() + .w(size.width) + .h(size.height) + .bg(self.color.opacity(0.5)) + .text_color(gpui::white()) + .text_xs() + .shadow_md() + .child(format!("Item {}", self.ix)), + ) + } +} + +struct DragDrop { + drop_on: Option, +} + +impl DragDrop { + fn new() -> Self { + Self { drop_on: None } + } +} + +impl Render for DragDrop { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let items = [gpui::blue(), gpui::red(), gpui::green()]; + + div() + .size_full() + .flex() + .flex_col() + .gap_5() + .bg(gpui::white()) + .justify_center() + .items_center() + .text_color(rgb(0x333333)) + .child(div().text_xl().text_center().child("Drop & Drop")) + .child( + div() + .w_full() + .mb_10() + .justify_center() + .flex() + .flex_row() + .gap_4() + .items_center() + .children(items.into_iter().enumerate().map(|(ix, color)| { + let drag_info = DragInfo::new(ix, color); + + div() + .id(("item", ix)) + .size_32() + .flex() + .justify_center() + .items_center() + .border_2() + .border_color(color) + .text_color(color) + .cursor_move() + .hover(|this| this.bg(color.opacity(0.2))) + .child(format!("Item ({})", ix)) + .on_drag(drag_info, |info: &DragInfo, position, _, cx| { + cx.new(|_| info.position(position)) + }) + })), + ) + .child( + div() + .id("drop-target") + .w_128() + .h_32() + .flex() + .justify_center() + .items_center() + .border_3() + .border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black())) + .when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5))) + .on_drop(cx.listener(|this, info: &DragInfo, _, _| { + this.drop_on = Some(*info); + })) + .child("Drop items here"), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| DragDrop::new()), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/example_support/fonts.rs b/vendor/gpui-pre/examples/example_support/fonts.rs new file mode 100644 index 0000000..39c3be1 --- /dev/null +++ b/vendor/gpui-pre/examples/example_support/fonts.rs @@ -0,0 +1,44 @@ +#[cfg(target_family = "wasm")] +use std::borrow::Cow; + +use gpui::App; + +#[cfg(target_family = "wasm")] +pub fn load_fonts(cx: &App) -> bool { + let fonts = [ + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") + .as_slice(), + ), + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf") + .as_slice(), + ), + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf") + .as_slice(), + ), + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf") + .as_slice(), + ), + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/lilex/Lilex-Regular.ttf").as_slice(), + ), + Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Bold.ttf").as_slice()), + Cow::Borrowed(include_bytes!("../../../../assets/fonts/lilex/Lilex-Italic.ttf").as_slice()), + Cow::Borrowed( + include_bytes!("../../../../assets/fonts/lilex/Lilex-BoldItalic.ttf").as_slice(), + ), + ]; + if let Err(error) = cx.text_system().add_fonts(fonts.into()) { + web_sys::console::error_1(&format!("failed to load application fonts: {error:#}").into()); + return false; + } + true +} + +#[cfg(not(target_family = "wasm"))] +pub fn load_fonts(_cx: &App) -> bool { + true +} diff --git a/vendor/gpui-pre/examples/focus_visible.rs b/vendor/gpui-pre/examples/focus_visible.rs new file mode 100644 index 0000000..c5b3d5d --- /dev/null +++ b/vendor/gpui-pre/examples/focus_visible.rs @@ -0,0 +1,235 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, + WindowBounds, WindowOptions, actions, div, prelude::*, px, size, +}; +use gpui_platform::application; + +actions!(example, [Tab, TabPrev, Quit]); + +struct Example { + focus_handle: FocusHandle, + items: Vec<(FocusHandle, &'static str)>, + message: SharedString, +} + +impl Example { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let items = vec![ + ( + cx.focus_handle().tab_index(1).tab_stop(true), + "Button with .focus() - always shows border when focused", + ), + ( + cx.focus_handle().tab_index(2).tab_stop(true), + "Button with .focus_visible() - only shows border with keyboard", + ), + ( + cx.focus_handle().tab_index(3).tab_stop(true), + "Button with both .focus() and .focus_visible()", + ), + ]; + + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle, cx); + + Self { + focus_handle, + items, + message: SharedString::from( + "Try clicking vs tabbing! Click shows no border, Tab shows border.", + ), + } + } + + fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + window.focus_next(cx); + self.message = SharedString::from("Pressed Tab - focus-visible border should appear!"); + } + + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { + window.focus_prev(cx); + self.message = + SharedString::from("Pressed Shift-Tab - focus-visible border should appear!"); + } + + fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context) { + cx.quit(); + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn button_base(id: impl Into, label: &'static str) -> Stateful
{ + div() + .id(id) + .h_16() + .w_full() + .flex() + .justify_center() + .items_center() + .bg(gpui::rgb(0x2563eb)) + .text_color(gpui::white()) + .rounded_md() + .cursor_pointer() + .hover(|style| style.bg(gpui::rgb(0x1d4ed8))) + .child(label) + } + + div() + .id("app") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) + .on_action(cx.listener(Self::on_quit)) + .size_full() + .flex() + .flex_col() + .p_8() + .gap_6() + .bg(gpui::rgb(0xf3f4f6)) + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x111827)) + .child("CSS focus-visible Demo"), + ) + .child( + div() + .p_4() + .rounded_md() + .bg(gpui::rgb(0xdbeafe)) + .text_color(gpui::rgb(0x1e3a8a)) + .child(self.message.clone()), + ) + .child( + div() + .flex() + .flex_col() + .gap_4() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child("1. Regular .focus() - always visible:"), + ) + .child( + button_base("button1", self.items[0].1) + .track_focus(&self.items[0].0) + .focus(|style| { + style.border_4().border_color(gpui::rgb(0xfbbf24)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 1 - focus border is visible!".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child("2. New .focus_visible() - only keyboard:"), + ) + .child( + button_base("button2", self.items[1].1) + .track_focus(&self.items[1].0) + .focus_visible(|style| { + style.border_4().border_color(gpui::rgb(0x10b981)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 2 - no border! Try Tab instead.".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child( + "3. Both .focus() (yellow) and .focus_visible() (green):", + ), + ) + .child( + button_base("button3", self.items[2].1) + .track_focus(&self.items[2].0) + .focus(|style| { + style.border_4().border_color(gpui::rgb(0xfbbf24)) + }) + .focus_visible(|style| { + style.border_4().border_color(gpui::rgb(0x10b981)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 3 - yellow border. Tab shows green!" + .into(); + cx.notify(); + })), + ), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.bind_keys([ + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + KeyBinding::new("cmd-q", Quit, None), + ]); + + let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/gif_viewer.rs b/vendor/gpui-pre/examples/gif_viewer.rs new file mode 100644 index 0000000..80b56aa --- /dev/null +++ b/vendor/gpui-pre/examples/gif_viewer.rs @@ -0,0 +1,62 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{App, Context, Render, Window, WindowOptions, div, img, prelude::*}; +use gpui_platform::application; +use std::path::PathBuf; + +struct GifViewer { + gif_path: PathBuf, +} + +impl GifViewer { + fn new(gif_path: PathBuf) -> Self { + Self { gif_path } + } +} + +impl Render for GifViewer { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child( + img(self.gif_path.clone()) + .size_full() + .object_fit(gpui::ObjectFit::Contain) + .id("gif"), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let gif_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif"); + + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |_, cx| cx.new(|_| GifViewer::new(gif_path)), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + env_logger::init(); + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/gradient.rs b/vendor/gpui-pre/examples/gradient.rs new file mode 100644 index 0000000..df1398f --- /dev/null +++ b/vendor/gpui-pre/examples/gradient.rs @@ -0,0 +1,278 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, ColorSpace, Context, Half, Render, Window, WindowOptions, canvas, div, + linear_color_stop, linear_gradient, point, prelude::*, px, size, +}; +use gpui_platform::application; + +struct GradientViewer { + color_space: ColorSpace, +} + +impl GradientViewer { + fn new() -> Self { + Self { + color_space: ColorSpace::default(), + } + } +} + +impl Render for GradientViewer { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let color_space = self.color_space; + + div() + .bg(gpui::white()) + .size_full() + .p_4() + .flex() + .flex_col() + .gap_3() + .child( + div() + .flex() + .gap_2() + .justify_between() + .items_center() + .child("Gradient Examples") + .child( + div().flex().gap_2().items_center().child( + div() + .id("method") + .flex() + .px_3() + .py_1() + .text_sm() + .bg(gpui::black()) + .text_color(gpui::white()) + .child(format!("{}", color_space)) + .active(|this| this.opacity(0.8)) + .on_click(cx.listener(move |this, _, _, cx| { + this.color_space = match this.color_space { + ColorSpace::Oklab => ColorSpace::Srgb, + ColorSpace::Srgb => ColorSpace::Oklab, + }; + cx.notify(); + })), + ), + ), + ) + .child( + div() + .flex() + .flex_1() + .gap_3() + .child( + div() + .size_full() + .rounded_xl() + .flex() + .items_center() + .justify_center() + .bg(gpui::red()) + .text_color(gpui::white()) + .child("Solid Color"), + ) + .child( + div() + .size_full() + .rounded_xl() + .flex() + .items_center() + .justify_center() + .bg(gpui::blue()) + .text_color(gpui::white()) + .child("Solid Color"), + ), + ) + .child( + div() + .flex() + .flex_1() + .gap_3() + .h_24() + .text_color(gpui::white()) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 45., + linear_color_stop(gpui::red(), 0.), + linear_color_stop(gpui::blue(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 135., + linear_color_stop(gpui::red(), 0.), + linear_color_stop(gpui::green(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 225., + linear_color_stop(gpui::green(), 0.), + linear_color_stop(gpui::blue(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 315., + linear_color_stop(gpui::green(), 0.), + linear_color_stop(gpui::yellow(), 1.), + ) + .color_space(color_space)), + ), + ) + .child( + div() + .flex() + .flex_1() + .gap_3() + .h_24() + .text_color(gpui::white()) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 0., + linear_color_stop(gpui::red(), 0.), + linear_color_stop(gpui::white(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 90., + linear_color_stop(gpui::blue(), 0.), + linear_color_stop(gpui::white(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 180., + linear_color_stop(gpui::green(), 0.), + linear_color_stop(gpui::white(), 1.), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 360., + linear_color_stop(gpui::yellow(), 0.), + linear_color_stop(gpui::white(), 1.), + ) + .color_space(color_space)), + ), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 0., + linear_color_stop(gpui::green(), 0.05), + linear_color_stop(gpui::yellow(), 0.95), + ) + .color_space(color_space)), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 90., + linear_color_stop(gpui::blue(), 0.05), + linear_color_stop(gpui::red(), 0.95), + ) + .color_space(color_space)), + ) + .child( + div() + .flex() + .flex_1() + .gap_3() + .child( + div().flex().flex_1().gap_3().child( + div().flex_1().rounded_xl().bg(linear_gradient( + 90., + linear_color_stop(gpui::blue(), 0.5), + linear_color_stop(gpui::red(), 0.5), + ) + .color_space(color_space)), + ), + ) + .child( + div().flex_1().rounded_xl().bg(linear_gradient( + 180., + linear_color_stop(gpui::green(), 0.), + linear_color_stop(gpui::blue(), 0.5), + ) + .color_space(color_space)), + ), + ) + .child(div().h_24().child(canvas( + move |_, _, _| {}, + move |bounds, _, window, _| { + let size = size(bounds.size.width * 0.8, px(80.)); + let square_bounds = Bounds { + origin: point( + bounds.size.width.half() - size.width.half(), + bounds.origin.y, + ), + size, + }; + let height = square_bounds.size.height; + let horizontal_offset = height; + let vertical_offset = px(30.); + let mut builder = gpui::PathBuilder::fill(); + builder.move_to(square_bounds.bottom_left()); + builder + .line_to(square_bounds.origin + point(horizontal_offset, vertical_offset)); + builder.line_to( + square_bounds.top_right() + point(-horizontal_offset, vertical_offset), + ); + + builder.line_to(square_bounds.bottom_right()); + builder.line_to(square_bounds.bottom_left()); + let path = builder.build().unwrap(); + window.paint_path( + path, + linear_gradient( + 180., + linear_color_stop(gpui::red(), 0.), + linear_color_stop(gpui::blue(), 1.), + ) + .color_space(color_space), + ); + }, + ))) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |_, cx| cx.new(|_| GradientViewer::new()), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/grid_layout.rs b/vendor/gpui-pre/examples/grid_layout.rs new file mode 100644 index 0000000..51f9b13 --- /dev/null +++ b/vendor/gpui-pre/examples/grid_layout.rs @@ -0,0 +1,95 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, container_query, div, + prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +// https://en.wikipedia.org/wiki/Holy_grail_(web_design) +// +// Resize the window: the layout is chosen by `container_query` based on the +// measured size of the container, collapsing to a single stacked column when +// it becomes too narrow for the three-column grid. +struct HolyGrailExample {} + +impl Render for HolyGrailExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + container_query(|container_size, _window, _cx| { + let block = |color: Hsla| { + div() + .size_full() + .bg(color) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()) + .items_center() + }; + + let header = block(gpui::white()).child(format!("Header — {}", container_size.width)); + let table_of_contents = block(gpui::red()).child("Table of contents"); + let content = block(gpui::green()).child("Content"); + let ad = block(gpui::blue()).child("AD :(").text_color(gpui::white()); + let footer = block(gpui::black()) + .text_color(gpui::white()) + .child("Footer"); + + let container = div().gap_1().bg(rgb(0x505050)).shadow_lg().size_full(); + + if container_size.width < px(400.) { + container + .flex() + .flex_col() + .child(header.h_12().flex_none()) + .child(table_of_contents.h_20().flex_none()) + .child(content.flex_1()) + .child(ad.h_20().flex_none()) + .child(footer.h_12().flex_none()) + } else { + container + .grid() + .grid_cols(5) + .grid_rows(5) + .child(header.row_span(1).col_span_full()) + .child(table_of_contents.col_span(1).h_56()) + .child(content.col_span(3).row_span(3)) + .child(ad.col_span(1).row_span(3)) + .child(footer.row_span(1).col_span_full()) + } + }) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| HolyGrailExample {}), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/hello_world.rs b/vendor/gpui-pre/examples/hello_world.rs new file mode 100644 index 0000000..d18cb04 --- /dev/null +++ b/vendor/gpui-pre/examples/hello_world.rs @@ -0,0 +1,127 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, prelude::*, px, + rgb, size, +}; +use gpui_platform::application; + +struct HelloWorld { + text: SharedString, +} + +impl Render for HelloWorld { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child(format!("Hello, {}!", self.text)) + .child( + div() + .flex() + .gap_2() + .child( + div() + .size_8() + .bg(gpui::red()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::green()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::blue()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::yellow()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::black()) + .border_1() + .border_dashed() + .rounded_md() + .rounded_md() + .border_color(gpui::white()), + ) + .child( + div() + .size_8() + .bg(gpui::white()) + .border_1() + .border_dashed() + .rounded_md() + .border_color(gpui::black()), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| HelloWorld { + text: "World".into(), + }) + }, + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/image/app-icon.png b/vendor/gpui-pre/examples/image/app-icon.png new file mode 100644 index 0000000..08b6d8a Binary files /dev/null and b/vendor/gpui-pre/examples/image/app-icon.png differ diff --git a/vendor/gpui-pre/examples/image/arrow_circle.svg b/vendor/gpui-pre/examples/image/arrow_circle.svg new file mode 100644 index 0000000..90e352b --- /dev/null +++ b/vendor/gpui-pre/examples/image/arrow_circle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/vendor/gpui-pre/examples/image/black-cat-typing.gif b/vendor/gpui-pre/examples/image/black-cat-typing.gif new file mode 100644 index 0000000..671a102 Binary files /dev/null and b/vendor/gpui-pre/examples/image/black-cat-typing.gif differ diff --git a/vendor/gpui-pre/examples/image/color.svg b/vendor/gpui-pre/examples/image/color.svg new file mode 100644 index 0000000..a080681 --- /dev/null +++ b/vendor/gpui-pre/examples/image/color.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/vendor/gpui-pre/examples/image/exif-orientation-rotate-180.jpg b/vendor/gpui-pre/examples/image/exif-orientation-rotate-180.jpg new file mode 100644 index 0000000..9c09565 Binary files /dev/null and b/vendor/gpui-pre/examples/image/exif-orientation-rotate-180.jpg differ diff --git a/vendor/gpui-pre/examples/image/image.rs b/vendor/gpui-pre/examples/image/image.rs new file mode 100644 index 0000000..30160c6 --- /dev/null +++ b/vendor/gpui-pre/examples/image/image.rs @@ -0,0 +1,223 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "../example_support/fonts.rs"] +mod example_support; + +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use gpui::{ + App, AppContext, AssetSource, Bounds, Context, ImageSource, KeyBinding, Menu, MenuItem, Point, + SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div, + img, prelude::*, px, rgb, size, +}; +#[cfg(not(target_family = "wasm"))] +use reqwest_client::ReqwestClient; + +struct Assets { + base: PathBuf, +} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + fs::read(self.base.join(path)) + .map(|data| Some(std::borrow::Cow::Owned(data))) + .map_err(|e| e.into()) + } + + fn list(&self, path: &str) -> Result> { + fs::read_dir(self.base.join(path)) + .map(|entries| { + entries + .filter_map(|entry| { + entry + .ok() + .and_then(|entry| entry.file_name().into_string().ok()) + .map(SharedString::from) + }) + .collect() + }) + .map_err(|e| e.into()) + } +} + +#[derive(IntoElement)] +struct ImageContainer { + text: SharedString, + src: ImageSource, +} + +impl ImageContainer { + pub fn new(text: impl Into, src: impl Into) -> Self { + Self { + text: text.into(), + src: src.into(), + } + } +} + +impl RenderOnce for ImageContainer { + fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { + div().child( + div() + .flex_row() + .size_full() + .gap_4() + .child(self.text) + .child(img(self.src).size(px(256.0))), + ) + } +} + +struct ImageShowcase { + local_resource: Arc, + remote_resource: SharedUri, + asset_resource: SharedString, +} + +impl Render for ImageShowcase { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .id("main") + .bg(gpui::white()) + .overflow_y_scroll() + .p_5() + .size_full() + .child( + div() + .flex() + .flex_col() + .justify_center() + .items_center() + .gap_8() + .child(img( + "https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg", + )) + .child( + div() + .flex() + .flex_row() + .justify_center() + .items_center() + .gap_8() + .child(ImageContainer::new( + "Image loaded from a local file with EXIF orientation", + self.local_resource.clone(), + )) + .child(ImageContainer::new( + "Image loaded from a remote resource", + self.remote_resource.clone(), + )) + .child(ImageContainer::new( + "Image loaded from an asset", + self.asset_resource.clone(), + )), + ) + .child( + div() + .flex() + .flex_row() + .gap_8() + .child( + div() + .flex_col() + .child("Auto Width") + .child(img("https://picsum.photos/800/400").h(px(180.))), + ) + .child( + div() + .flex_col() + .child("Auto Height") + .child(img("https://picsum.photos/800/400").w(px(180.))), + ), + ) + .child( + div() + .flex() + .flex_col() + .justify_center() + .items_center() + .w_full() + .border_1() + .border_color(rgb(0xC0C0C0)) + .child("image with max width 100%") + .child(img("https://picsum.photos/800/400").max_w_full()), + ), + ) + } +} + +actions!(image, [Quit]); + +fn run_example() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + #[cfg(not(target_family = "wasm"))] + let app = gpui_platform::application(); + #[cfg(target_family = "wasm")] + let app = gpui_platform::application(); + app.with_assets(Assets { + base: manifest_dir.join("examples"), + }) + .run(move |cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + #[cfg(not(target_family = "wasm"))] + { + let http_client = ReqwestClient::user_agent("gpui example").unwrap(); + cx.set_http_client(Arc::new(http_client)); + } + + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.set_menus(vec![Menu { + name: "Image".into(), + items: vec![MenuItem::action("Quit", Quit)], + disabled: false, + }]); + + let window_options = WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Image Example")), + appears_transparent: false, + ..Default::default() + }), + + window_bounds: Some(WindowBounds::Windowed(Bounds { + size: size(px(1100.), px(600.)), + origin: Point::new(px(200.), px(200.)), + })), + + ..Default::default() + }; + + cx.open_window(window_options, |_, cx| { + cx.new(|_| ImageShowcase { + // Relative path to your root project path + local_resource: manifest_dir + .join("examples/image/exif-orientation-rotate-180.jpg") + .into(), + remote_resource: "https://picsum.photos/800/400".into(), + asset_resource: "image/color.svg".into(), + }) + }) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + env_logger::init(); + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/image_gallery.rs b/vendor/gpui-pre/examples/image_gallery.rs new file mode 100644 index 0000000..d1cb919 --- /dev/null +++ b/vendor/gpui-pre/examples/image_gallery.rs @@ -0,0 +1,299 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, AppContext, Bounds, ClickEvent, Context, ElementId, Entity, ImageCache, + ImageCacheProvider, KeyBinding, Menu, MenuItem, RetainAllImageCache, SharedString, + TitlebarOptions, Window, WindowBounds, WindowOptions, actions, div, hash, image_cache, img, + prelude::*, px, rgb, size, +}; +#[cfg(not(target_family = "wasm"))] +use reqwest_client::ReqwestClient; +use std::{collections::HashMap, sync::Arc}; + +const IMAGES_IN_GALLERY: usize = 30; + +struct ImageGallery { + image_key: String, + items_count: usize, + total_count: usize, + image_cache: Entity, +} + +impl ImageGallery { + fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + self.image_cache + .update(cx, |image_cache, cx| image_cache.clear(window, cx)); + + let t = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + + self.image_key = format!("{}", t); + self.total_count += self.items_count; + cx.notify(); + } +} + +impl Render for ImageGallery { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let image_url: SharedString = + format!("https://picsum.photos/400/200?t={}", self.image_key).into(); + + div() + .flex() + .flex_col() + .text_color(gpui::white()) + .child("Manually managed image cache:") + .child( + div() + .image_cache(self.image_cache.clone()) + .id("main") + .text_color(gpui::black()) + .bg(rgb(0xE9E9E9)) + .overflow_y_scroll() + .p_4() + .size_full() + .flex() + .flex_col() + .items_center() + .gap_2() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_between() + .child(format!( + "Example to show images and test memory usage (Rendered: {} images).", + self.total_count + )) + .child( + div() + .id("btn") + .py_1() + .px_4() + .bg(gpui::black()) + .hover(|this| this.opacity(0.8)) + .text_color(gpui::white()) + .text_center() + .w_40() + .child("Next Photos") + .on_click(cx.listener(Self::on_next_image)), + ), + ) + .child( + div() + .id("image-gallery") + .flex() + .flex_row() + .flex_wrap() + .gap_x_4() + .gap_y_2() + .justify_around() + .children( + (0..self.items_count) + .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), + ), + ), + ) + .child( + "Automatically managed image cache:" + ) + .child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child( + div() + .id("main") + .bg(rgb(0xE9E9E9)) + .text_color(gpui::black()) + .overflow_y_scroll() + .p_4() + .size_full() + .flex() + .flex_col() + .items_center() + .gap_2() + .child( + div() + .id("image-gallery") + .flex() + .flex_row() + .flex_wrap() + .gap_x_4() + .gap_y_2() + .justify_around() + .children( + (0..self.items_count) + .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), + ), + ) + )) + } +} + +fn simple_lru_cache(id: impl Into, max_items: usize) -> SimpleLruCacheProvider { + SimpleLruCacheProvider { + id: id.into(), + max_items, + } +} + +struct SimpleLruCacheProvider { + id: ElementId, + max_items: usize, +} + +impl ImageCacheProvider for SimpleLruCacheProvider { + fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache { + window + .with_global_id(self.id.clone(), |global_id, window| { + window.with_element_state::, _>( + global_id, + |lru_cache, _window| { + let mut lru_cache = lru_cache.unwrap_or_else(|| { + cx.new(|cx| SimpleLruCache::new(self.max_items, cx)) + }); + if lru_cache.read(cx).max_items != self.max_items { + lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx)); + } + (lru_cache.clone(), lru_cache) + }, + ) + }) + .into() + } +} + +struct SimpleLruCache { + max_items: usize, + usages: Vec, + cache: HashMap, +} + +impl SimpleLruCache { + fn new(max_items: usize, cx: &mut Context) -> Self { + cx.on_release(|simple_cache, cx| { + for (_, item) in std::mem::take(&mut simple_cache.cache) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, None); + } + } + }) + .detach(); + + Self { + max_items, + usages: Vec::with_capacity(max_items), + cache: HashMap::with_capacity(max_items), + } + } +} + +impl ImageCache for SimpleLruCache { + fn load( + &mut self, + resource: &gpui::Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, gpui::ImageCacheError>> { + assert_eq!(self.usages.len(), self.cache.len()); + assert!(self.cache.len() <= self.max_items); + + let hash = hash(resource); + + if let Some(item) = self.cache.get_mut(&hash) { + let current_ix = self + .usages + .iter() + .position(|item| *item == hash) + .expect("cache and usages must stay in sync"); + self.usages.remove(current_ix); + self.usages.insert(0, hash); + + return item.use_image(window); + } + + if self.usages.len() == self.max_items { + let oldest = self.usages.pop().unwrap(); + let image = self + .cache + .remove(&oldest) + .expect("cache and usages must be in sync"); + if let Some(Ok(image)) = image.get() { + cx.drop_image(image, Some(window)); + } + } + let item = gpui::ImageCacheItem::new(resource, cx); + let result = item.use_image(window); + self.cache.insert(hash, item); + self.usages.insert(0, hash); + + result + } +} + +actions!(image, [Quit]); + +fn run_example() { + #[cfg(not(target_family = "wasm"))] + let app = gpui_platform::application(); + #[cfg(target_family = "wasm")] + let app = gpui_platform::single_threaded_web(); + + app.run(move |cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + #[cfg(not(target_family = "wasm"))] + { + let http_client = ReqwestClient::user_agent("gpui example").unwrap(); + cx.set_http_client(Arc::new(http_client)); + } + + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.set_menus([Menu::new("Image Gallery").items([MenuItem::action("Quit", Quit)])]); + + let window_options = WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Image Gallery")), + appears_transparent: false, + ..Default::default() + }), + + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(1100.), px(860.)), + cx, + ))), + + ..Default::default() + }; + + cx.open_window(window_options, |_, cx| { + cx.new(|ctx| ImageGallery { + image_key: "".into(), + items_count: IMAGES_IN_GALLERY, + total_count: 0, + image_cache: RetainAllImageCache::new(ctx), + }) + }) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + env_logger::init(); + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/image_loading.rs b/vendor/gpui-pre/examples/image_loading.rs new file mode 100644 index 0000000..b682891 --- /dev/null +++ b/vendor/gpui-pre/examples/image_loading.rs @@ -0,0 +1,232 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::{path::Path, sync::Arc, time::Duration}; + +use gpui::{ + Animation, AnimationExt, App, Asset, AssetLogger, AssetSource, Bounds, Context, Hsla, + ImageAssetLoader, ImageCacheError, ImgResourceLoader, LOADING_DELAY, Length, RenderImage, + Resource, SharedString, Window, WindowBounds, WindowOptions, black, div, img, prelude::*, + pulsating_between, px, red, size, +}; +use gpui_platform::application; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> anyhow::Result>> { + std::fs::read(path) + .map(Into::into) + .map_err(Into::into) + .map(Some) + } + + fn list(&self, path: &str) -> anyhow::Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().into_owned(), + )) + }) + .collect::>()) + } +} + +const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png"); + +#[derive(Copy, Clone, Hash)] +struct LoadImageParameters { + timeout: Duration, + fail: bool, +} + +struct LoadImageWithParameters {} + +impl Asset for LoadImageWithParameters { + type Source = LoadImageParameters; + + type Output = Result, ImageCacheError>; + + fn load( + parameters: Self::Source, + cx: &mut App, + ) -> impl std::future::Future + Send + 'static { + let timer = cx.background_executor().timer(parameters.timeout); + let data = AssetLogger::::load( + Resource::Path(Path::new(IMAGE).to_path_buf().into()), + cx, + ); + async move { + timer.await; + if parameters.fail { + log::error!("Intentionally failed to load image"); + Err(anyhow::anyhow!("Failed to load image").into()) + } else { + data.await + } + } + } +} + +struct ImageLoadingExample {} + +impl ImageLoadingExample { + fn loading_element() -> impl IntoElement { + div().size_full().flex_none().p_0p5().rounded_xs().child( + div().size_full().with_animation( + "loading-bg", + Animation::new(Duration::from_secs(3)) + .repeat() + .with_easing(pulsating_between(0.04, 0.24)), + move |this, delta| this.bg(black().opacity(delta)), + ), + ) + } + + fn fallback_element() -> impl IntoElement { + let fallback_color: Hsla = black().opacity(0.5); + + div().size_full().flex_none().p_0p5().child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .rounded_xs() + .text_sm() + .text_color(fallback_color) + .border_1() + .border_color(fallback_color) + .child("?"), + ) + } +} + +impl Render for ImageLoadingExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().flex().flex_col().size_full().justify_around().child( + div().flex().flex_row().w_full().justify_around().child( + div() + .flex() + .bg(gpui::white()) + .size(Length::Definite(px(300.0).into())) + .justify_center() + .items_center() + .child({ + let image_source = LoadImageParameters { + timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)), + fail: false, + }; + + // Load within the 'loading delay', should not show loading fallback + img(move |window: &mut Window, cx: &mut App| { + window.use_asset::(&image_source, cx) + }) + .id("image-1") + .border_1() + .size_12() + .with_fallback(|| Self::fallback_element().into_any_element()) + .border_color(red()) + .with_loading(|| Self::loading_element().into_any_element()) + .on_click(move |_, _, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Load after a long delay + let image_source = LoadImageParameters { + timeout: Duration::from_secs(5), + fail: false, + }; + + img(move |window: &mut Window, cx: &mut App| { + window.use_asset::(&image_source, cx) + }) + .id("image-2") + .with_fallback(|| Self::fallback_element().into_any_element()) + .with_loading(|| Self::loading_element().into_any_element()) + .size_12() + .border_1() + .border_color(red()) + .on_click(move |_, _, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Fail to load image after a long delay + let image_source = LoadImageParameters { + timeout: Duration::from_secs(5), + fail: true, + }; + + // Fail to load after a long delay + img(move |window: &mut Window, cx: &mut App| { + window.use_asset::(&image_source, cx) + }) + .id("image-3") + .with_fallback(|| Self::fallback_element().into_any_element()) + .with_loading(|| Self::loading_element().into_any_element()) + .size_12() + .border_1() + .border_color(red()) + .on_click(move |_, _, cx| { + cx.remove_asset::(&image_source); + }) + }) + .child({ + // Ensure that the normal image loader doesn't spam logs + let image_source = Path::new( + "this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope", + ) + .to_path_buf(); + img(image_source.clone()) + .id("image-4") + .border_1() + .size_12() + .with_fallback(|| Self::fallback_element().into_any_element()) + .border_color(red()) + .with_loading(|| Self::loading_element().into_any_element()) + .on_click(move |_, _, cx| { + cx.remove_asset::(&image_source.clone().into()); + }) + }), + ), + ) + } +} + +fn run_example() { + application().with_assets(Assets {}).run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(300.), px(300.)), + cx, + ))), + ..Default::default() + }; + cx.open_window(options, |_, cx| { + cx.activate(false); + cx.new(|_| ImageLoadingExample {}) + }) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + env_logger::init(); + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/input.rs b/vendor/gpui-pre/examples/input.rs new file mode 100644 index 0000000..eadc2d2 --- /dev/null +++ b/vendor/gpui-pre/examples/input.rs @@ -0,0 +1,784 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::ops::Range; + +use gpui::{ + App, Bounds, ClipboardItem, Context, CursorStyle, ElementId, ElementInputHandler, Entity, + EntityInputHandler, FocusHandle, Focusable, GlobalElementId, KeyBinding, Keystroke, LayoutId, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, + ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, WindowBounds, + WindowOptions, actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative, + rgb, rgba, size, white, yellow, +}; +use gpui_platform::application; +use unicode_segmentation::*; + +actions!( + text_input, + [ + Backspace, + Delete, + Left, + Right, + SelectLeft, + SelectRight, + SelectAll, + Home, + End, + ShowCharacterPalette, + Paste, + Cut, + Copy, + Quit, + ] +); + +struct TextInput { + focus_handle: FocusHandle, + content: SharedString, + placeholder: SharedString, + selected_range: Range, + selection_reversed: bool, + marked_range: Option>, + last_layout: Option, + last_bounds: Option>, + is_selecting: bool, +} + +impl TextInput { + fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context) { + if self.selected_range.is_empty() { + self.move_to(self.previous_boundary(self.cursor_offset()), cx); + } else { + self.move_to(self.selected_range.start, cx) + } + } + + fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context) { + if self.selected_range.is_empty() { + self.move_to(self.next_boundary(self.selected_range.end), cx); + } else { + self.move_to(self.selected_range.end, cx) + } + } + + fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context) { + self.select_to(self.previous_boundary(self.cursor_offset()), cx); + } + + fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context) { + self.select_to(self.next_boundary(self.cursor_offset()), cx); + } + + fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context) { + self.move_to(0, cx); + self.select_to(self.content.len(), cx) + } + + fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context) { + self.move_to(0, cx); + } + + fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context) { + self.move_to(self.content.len(), cx); + } + + fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + if self.selected_range.is_empty() { + let prev = self.previous_boundary(self.cursor_offset()); + if self.cursor_offset() == prev { + window.play_system_bell(); + return; + } + self.select_to(prev, cx) + } + self.replace_text_in_range(None, "", window, cx) + } + + fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + if self.selected_range.is_empty() { + let next = self.next_boundary(self.cursor_offset()); + if self.cursor_offset() == next { + window.play_system_bell(); + return; + } + self.select_to(next, cx) + } + self.replace_text_in_range(None, "", window, cx) + } + + fn on_mouse_down( + &mut self, + event: &MouseDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.is_selecting = true; + + if event.modifiers.shift { + self.select_to(self.index_for_mouse_position(event.position), cx); + } else { + self.move_to(self.index_for_mouse_position(event.position), cx) + } + } + + fn on_mouse_up(&mut self, _: &MouseUpEvent, _window: &mut Window, _: &mut Context) { + self.is_selecting = false; + } + + fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context) { + if self.is_selecting { + self.select_to(self.index_for_mouse_position(event.position), cx); + } + } + + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _: &mut Context, + ) { + window.show_character_palette(); + } + + fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { + self.replace_text_in_range(None, &text.replace("\n", " "), window, cx); + } + } + + fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + self.content[self.selected_range.clone()].to_string(), + )); + } + } + fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + self.content[self.selected_range.clone()].to_string(), + )); + self.replace_text_in_range(None, "", window, cx) + } + } + + fn move_to(&mut self, offset: usize, cx: &mut Context) { + self.selected_range = offset..offset; + cx.notify() + } + + fn cursor_offset(&self) -> usize { + if self.selection_reversed { + self.selected_range.start + } else { + self.selected_range.end + } + } + + fn index_for_mouse_position(&self, position: Point) -> usize { + if self.content.is_empty() { + return 0; + } + + let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) + else { + return 0; + }; + if position.y < bounds.top() { + return 0; + } + if position.y > bounds.bottom() { + return self.content.len(); + } + line.closest_index_for_x(position.x - bounds.left()) + } + + fn select_to(&mut self, offset: usize, cx: &mut Context) { + if self.selection_reversed { + self.selected_range.start = offset + } else { + self.selected_range.end = offset + }; + if self.selected_range.end < self.selected_range.start { + self.selection_reversed = !self.selection_reversed; + self.selected_range = self.selected_range.end..self.selected_range.start; + } + cx.notify() + } + + fn offset_from_utf16(&self, offset: usize) -> usize { + let mut utf8_offset = 0; + let mut utf16_count = 0; + + for ch in self.content.chars() { + if utf16_count >= offset { + break; + } + utf16_count += ch.len_utf16(); + utf8_offset += ch.len_utf8(); + } + + utf8_offset + } + + fn offset_to_utf16(&self, offset: usize) -> usize { + let mut utf16_offset = 0; + let mut utf8_count = 0; + + for ch in self.content.chars() { + if utf8_count >= offset { + break; + } + utf8_count += ch.len_utf8(); + utf16_offset += ch.len_utf16(); + } + + utf16_offset + } + + fn range_to_utf16(&self, range: &Range) -> Range { + self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) + } + + fn range_from_utf16(&self, range_utf16: &Range) -> Range { + self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) + } + + fn previous_boundary(&self, offset: usize) -> usize { + self.content + .grapheme_indices(true) + .rev() + .find_map(|(idx, _)| (idx < offset).then_some(idx)) + .unwrap_or(0) + } + + fn next_boundary(&self, offset: usize) -> usize { + self.content + .grapheme_indices(true) + .find_map(|(idx, _)| (idx > offset).then_some(idx)) + .unwrap_or(self.content.len()) + } + + fn reset(&mut self) { + self.content = "".into(); + self.selected_range = 0..0; + self.selection_reversed = false; + self.marked_range = None; + self.last_layout = None; + self.last_bounds = None; + self.is_selecting = false; + } +} + +impl EntityInputHandler for TextInput { + fn text_for_range( + &mut self, + range_utf16: Range, + actual_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let range = self.range_from_utf16(&range_utf16); + actual_range.replace(self.range_to_utf16(&range)); + Some(self.content[range].to_string()) + } + + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + Some(UTF16Selection { + range: self.range_to_utf16(&self.selected_range), + reversed: self.selection_reversed, + }) + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + self.marked_range + .as_ref() + .map(|range| self.range_to_utf16(range)) + } + + fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { + self.marked_range = None; + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + _: &mut Window, + cx: &mut Context, + ) { + let range = range_utf16 + .as_ref() + .map(|range_utf16| self.range_from_utf16(range_utf16)) + .or(self.marked_range.clone()) + .unwrap_or(self.selected_range.clone()); + + self.content = + (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) + .into(); + self.selected_range = range.start + new_text.len()..range.start + new_text.len(); + self.marked_range.take(); + cx.notify(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range_utf16: Option>, + _window: &mut Window, + cx: &mut Context, + ) { + let range = range_utf16 + .as_ref() + .map(|range_utf16| self.range_from_utf16(range_utf16)) + .or(self.marked_range.clone()) + .unwrap_or(self.selected_range.clone()); + + self.content = + (self.content[0..range.start].to_owned() + new_text + &self.content[range.end..]) + .into(); + if !new_text.is_empty() { + self.marked_range = Some(range.start..range.start + new_text.len()); + } else { + self.marked_range = None; + } + self.selected_range = new_selected_range_utf16 + .as_ref() + .map(|range_utf16| self.range_from_utf16(range_utf16)) + .map(|new_range| new_range.start + range.start..new_range.end + range.end) + .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); + + cx.notify(); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + let last_layout = self.last_layout.as_ref()?; + let range = self.range_from_utf16(&range_utf16); + Some(Bounds::from_corners( + point( + bounds.left() + last_layout.x_for_index(range.start), + bounds.top(), + ), + point( + bounds.left() + last_layout.x_for_index(range.end), + bounds.bottom(), + ), + )) + } + + fn character_index_for_point( + &mut self, + point: gpui::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let line_point = self.last_bounds?.localize(&point)?; + let last_layout = self.last_layout.as_ref()?; + + assert_eq!(last_layout.text, self.content); + let utf8_index = last_layout.index_for_x(point.x - line_point.x)?; + Some(self.offset_to_utf16(utf8_index)) + } +} + +struct TextElement { + input: Entity, +} + +struct PrepaintState { + line: Option, + cursor: Option, + selection: Option, +} + +impl IntoElement for TextElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for TextElement { + type RequestLayoutState = (); + type PrepaintState = PrepaintState; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = window.line_height().into(); + (window.request_layout(style, [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + let input = self.input.read(cx); + let content = input.content.clone(); + let selected_range = input.selected_range.clone(); + let cursor = input.cursor_offset(); + let style = window.text_style(); + + let (display_text, text_color) = if content.is_empty() { + (input.placeholder.clone(), hsla(0., 0., 0., 0.2)) + } else { + (content, style.color) + }; + + let run = TextRun { + len: display_text.len(), + font: style.font(), + color: text_color, + background_color: None, + underline: None, + strikethrough: None, + }; + let runs = if let Some(marked_range) = input.marked_range.as_ref() { + vec![ + TextRun { + len: marked_range.start, + ..run.clone() + }, + TextRun { + len: marked_range.end - marked_range.start, + underline: Some(UnderlineStyle { + color: Some(run.color), + thickness: px(1.0), + wavy: false, + }), + ..run.clone() + }, + TextRun { + len: display_text.len() - marked_range.end, + ..run + }, + ] + .into_iter() + .filter(|run| run.len > 0) + .collect() + } else { + vec![run] + }; + + let font_size = style.font_size.to_pixels(window.rem_size()); + let line = window + .text_system() + .shape_line(display_text, font_size, &runs, None); + + let cursor_pos = line.x_for_index(cursor); + let (selection, cursor) = if selected_range.is_empty() { + ( + None, + Some(fill( + Bounds::new( + point(bounds.left() + cursor_pos, bounds.top()), + size(px(2.), bounds.bottom() - bounds.top()), + ), + gpui::blue(), + )), + ) + } else { + ( + Some(fill( + Bounds::from_corners( + point( + bounds.left() + line.x_for_index(selected_range.start), + bounds.top(), + ), + point( + bounds.left() + line.x_for_index(selected_range.end), + bounds.bottom(), + ), + ), + rgba(0x3311ff30), + )), + None, + ) + }; + PrepaintState { + line: Some(line), + cursor, + selection, + } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let focus_handle = self.input.read(cx).focus_handle.clone(); + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.input.clone()), + cx, + ); + if let Some(selection) = prepaint.selection.take() { + window.paint_quad(selection) + } + let line = prepaint.line.take().unwrap(); + line.paint( + bounds.origin, + window.line_height(), + gpui::TextAlign::Left, + None, + window, + cx, + ) + .unwrap(); + + if focus_handle.is_focused(window) + && let Some(cursor) = prepaint.cursor.take() + { + window.paint_quad(cursor); + } + + self.input.update(cx, |input, _cx| { + input.last_layout = Some(line); + input.last_bounds = Some(bounds); + }); + } +} + +impl Render for TextInput { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .key_context("TextInput") + .track_focus(&self.focus_handle(cx)) + .cursor(CursorStyle::IBeam) + .on_action(cx.listener(Self::backspace)) + .on_action(cx.listener(Self::delete)) + .on_action(cx.listener(Self::left)) + .on_action(cx.listener(Self::right)) + .on_action(cx.listener(Self::select_left)) + .on_action(cx.listener(Self::select_right)) + .on_action(cx.listener(Self::select_all)) + .on_action(cx.listener(Self::home)) + .on_action(cx.listener(Self::end)) + .on_action(cx.listener(Self::show_character_palette)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_move(cx.listener(Self::on_mouse_move)) + .bg(rgb(0xeeeeee)) + .line_height(px(30.)) + .text_size(px(24.)) + .child( + div() + .h(px(30. + 4. * 2.)) + .w_full() + .p(px(4.)) + .bg(white()) + .child(TextElement { input: cx.entity() }), + ) + } +} + +impl Focusable for TextInput { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +struct InputExample { + text_input: Entity, + recent_keystrokes: Vec, + focus_handle: FocusHandle, +} + +impl Focusable for InputExample { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl InputExample { + fn on_reset_click(&mut self, _: &MouseUpEvent, _window: &mut Window, cx: &mut Context) { + self.recent_keystrokes.clear(); + self.text_input + .update(cx, |text_input, _cx| text_input.reset()); + cx.notify(); + } +} + +impl Render for InputExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .bg(rgb(0xaaaaaa)) + .track_focus(&self.focus_handle(cx)) + .flex() + .flex_col() + .size_full() + .child( + div() + .bg(white()) + .border_b_1() + .border_color(black()) + .flex() + .flex_row() + .justify_between() + .child(format!("Keyboard {}", cx.keyboard_layout().name())) + .child( + div() + .border_1() + .border_color(black()) + .px_2() + .bg(yellow()) + .child("Reset") + .hover(|style| { + style + .bg(yellow().blend(opaque_grey(0.5, 0.5))) + .cursor_pointer() + }) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)), + ), + ) + .child(self.text_input.clone()) + .children(self.recent_keystrokes.iter().rev().map(|ks| { + format!( + "{:} {}", + ks.unparse(), + if let Some(key_char) = ks.key_char.as_ref() { + format!("-> {:?}", key_char) + } else { + "".to_owned() + } + ) + })) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + cx.bind_keys([ + KeyBinding::new("backspace", Backspace, None), + KeyBinding::new("delete", Delete, None), + KeyBinding::new("left", Left, None), + KeyBinding::new("right", Right, None), + KeyBinding::new("shift-left", SelectLeft, None), + KeyBinding::new("shift-right", SelectRight, None), + KeyBinding::new("cmd-a", SelectAll, None), + KeyBinding::new("cmd-v", Paste, None), + KeyBinding::new("cmd-c", Copy, None), + KeyBinding::new("cmd-x", Cut, None), + KeyBinding::new("home", Home, None), + KeyBinding::new("end", End, None), + KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), + ]); + + let window = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + let text_input = cx.new(|cx| TextInput { + focus_handle: cx.focus_handle(), + content: "".into(), + placeholder: "Type here...".into(), + selected_range: 0..0, + selection_reversed: false, + marked_range: None, + last_layout: None, + last_bounds: None, + is_selecting: false, + }); + cx.new(|cx| InputExample { + text_input, + recent_keystrokes: vec![], + focus_handle: cx.focus_handle(), + }) + }, + ) + .unwrap(); + let view = window.update(cx, |_, _, cx| cx.entity()).unwrap(); + cx.observe_keystrokes(move |ev, _, cx| { + view.update(cx, |view, cx| { + view.recent_keystrokes.push(ev.keystroke.clone()); + cx.notify(); + }) + }) + .detach(); + cx.on_keyboard_layout_change({ + move |cx| { + window.update(cx, |_, _, cx| cx.notify()).ok(); + } + }) + .detach(); + + window + .update(cx, |view, window, cx| { + window.focus(&view.text_input.focus_handle(cx), cx); + cx.activate(true); + }) + .unwrap(); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/layer_shell.rs b/vendor/gpui-pre/examples/layer_shell.rs new file mode 100644 index 0000000..1437b05 --- /dev/null +++ b/vendor/gpui-pre/examples/layer_shell.rs @@ -0,0 +1,101 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +fn run_example() { + #[cfg(all(target_os = "linux", feature = "wayland"))] + example::main(); + + #[cfg(not(all(target_os = "linux", feature = "wayland")))] + panic!("This example requires the `wayland` feature and a linux system."); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} + +#[cfg(all(target_os = "linux", feature = "wayland"))] +mod example { + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + use gpui::{ + App, Bounds, Context, FontWeight, Size, Window, WindowBackgroundAppearance, WindowBounds, + WindowKind, WindowOptions, div, layer_shell::*, point, prelude::*, px, rems, rgba, white, + }; + use gpui_platform::application; + + struct LayerShellExample; + + impl LayerShellExample { + fn new(cx: &mut Context) -> Self { + cx.spawn(async move |this, cx| { + loop { + let _ = this.update(cx, |_, cx| cx.notify()); + cx.background_executor() + .timer(Duration::from_millis(500)) + .await; + } + }) + .detach(); + + LayerShellExample + } + } + + impl Render for LayerShellExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let hours = (now / 3600) % 24; + let minutes = (now / 60) % 60; + let seconds = now % 60; + + div() + .size_full() + .flex() + .items_center() + .justify_center() + .text_size(rems(4.5)) + .font_weight(FontWeight::EXTRA_BOLD) + .text_color(white()) + .bg(rgba(0x0000044)) + .rounded_xl() + .child(format!("{:02}:{:02}:{:02}", hours, minutes, seconds)) + } + } + + pub fn main() { + application().run(|cx: &mut App| { + cx.open_window( + WindowOptions { + titlebar: None, + window_bounds: Some(WindowBounds::Windowed(Bounds { + origin: point(px(0.), px(0.)), + size: Size::new(px(500.), px(200.)), + })), + app_id: Some("gpui-layer-shell-example".to_string()), + window_background: WindowBackgroundAppearance::Transparent, + kind: WindowKind::LayerShell(LayerShellOptions { + namespace: "gpui".to_string(), + anchor: Anchor::LEFT | Anchor::RIGHT | Anchor::BOTTOM, + margin: Some((px(0.), px(0.), px(40.), px(0.))), + keyboard_interactivity: KeyboardInteractivity::None, + ..Default::default() + }), + ..Default::default() + }, + |_, cx| cx.new(LayerShellExample::new), + ) + .unwrap(); + }); + } +} diff --git a/vendor/gpui-pre/examples/list_example.rs b/vendor/gpui-pre/examples/list_example.rs new file mode 100644 index 0000000..542b7e3 --- /dev/null +++ b/vendor/gpui-pre/examples/list_example.rs @@ -0,0 +1,176 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, ListAlignment, ListState, Render, Window, WindowBounds, WindowOptions, + div, list, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +const ITEM_COUNT: usize = 40; +const SCROLLBAR_WIDTH: f32 = 12.; + +struct BottomListDemo { + list_state: ListState, +} + +impl BottomListDemo { + fn new() -> Self { + Self { + list_state: ListState::new(ITEM_COUNT, ListAlignment::Bottom, px(500.)).measure_all(), + } + } +} + +impl Render for BottomListDemo { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let max_offset = self.list_state.max_offset_for_scrollbar().y; + let current_offset = -self.list_state.scroll_px_offset_for_scrollbar().y; + + let viewport_height = self.list_state.viewport_bounds().size.height; + + let raw_fraction = if max_offset > px(0.) { + current_offset / max_offset + } else { + 0. + }; + + let total_height = viewport_height + max_offset; + let thumb_height = if total_height > px(0.) { + px(viewport_height.as_f32() * viewport_height.as_f32() / total_height.as_f32()) + .max(px(30.)) + } else { + px(30.) + }; + + let track_space = viewport_height - thumb_height; + let thumb_top = track_space * raw_fraction; + + let bug_detected = raw_fraction > 1.0; + + div() + .size_full() + .bg(rgb(0xFFFFFF)) + .flex() + .flex_col() + .p_4() + .gap_2() + .child( + div() + .text_sm() + .flex() + .flex_col() + .gap_1() + .child(format!( + "offset: {:.0} / max: {:.0} | fraction: {:.3}", + current_offset.as_f32(), + max_offset.as_f32(), + raw_fraction, + )) + .child( + div() + .text_color(if bug_detected { + rgb(0xCC0000) + } else { + rgb(0x008800) + }) + .child(if bug_detected { + format!( + "BUG: fraction is {:.3} (> 1.0) — thumb is off-track!", + raw_fraction + ) + } else { + "OK: fraction <= 1.0 — thumb is within track.".to_string() + }), + ), + ) + .child( + div() + .flex_1() + .flex() + .flex_row() + .overflow_hidden() + .border_1() + .border_color(rgb(0xCCCCCC)) + .rounded_sm() + .child( + list(self.list_state.clone(), |index, _window, _cx| { + let height = px(30. + (index % 5) as f32 * 10.); + div() + .h(height) + .w_full() + .flex() + .items_center() + .px_3() + .border_b_1() + .border_color(rgb(0xEEEEEE)) + .bg(if index % 2 == 0 { + rgb(0xFAFAFA) + } else { + rgb(0xFFFFFF) + }) + .text_sm() + .child(format!("Item {index}")) + .into_any() + }) + .flex_1(), + ) + // Scrollbar track + .child( + div() + .w(px(SCROLLBAR_WIDTH)) + .h_full() + .flex_shrink_0() + .bg(rgb(0xE0E0E0)) + .relative() + .child( + // Thumb — position is unclamped to expose the bug + div() + .absolute() + .top(thumb_top) + .w_full() + .h(thumb_height) + .bg(if bug_detected { + rgb(0xCC0000) + } else { + rgb(0x888888) + }) + .rounded_sm(), + ), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(400.), px(500.)), cx); + cx.open_window( + WindowOptions { + focus: true, + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| BottomListDemo::new()), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/mouse_pressure.rs b/vendor/gpui-pre/examples/mouse_pressure.rs new file mode 100644 index 0000000..3470bf1 --- /dev/null +++ b/vendor/gpui-pre/examples/mouse_pressure.rs @@ -0,0 +1,87 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, MousePressureEvent, PressureStage, Window, WindowBounds, WindowOptions, + div, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +struct MousePressureExample { + pressure_stage: PressureStage, + pressure_amount: f32, +} + +impl Render for MousePressureExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child(format!("Pressure stage: {:?}", self.pressure_stage)) + .child(format!("Pressure amount: {:.2}", self.pressure_amount)) + .on_mouse_pressure(cx.listener(Self::on_mouse_pressure)) + } +} + +impl MousePressureExample { + fn on_mouse_pressure( + &mut self, + pressure_event: &MousePressureEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.pressure_amount = pressure_event.pressure; + self.pressure_stage = pressure_event.stage; + + cx.notify(); + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| { + cx.new(|_| MousePressureExample { + pressure_stage: PressureStage::Zero, + pressure_amount: 0.0, + }) + }, + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/move_entity_between_windows.rs b/vendor/gpui-pre/examples/move_entity_between_windows.rs new file mode 100644 index 0000000..e0f1646 --- /dev/null +++ b/vendor/gpui-pre/examples/move_entity_between_windows.rs @@ -0,0 +1,160 @@ +//! An entity registers callbacks via the `_in` API family and then gets +//! re-hosted in a new window via a click. The point of the example is to +//! demonstrate that callbacks dispatched after the move correctly target the +//! entity's *current* window rather than the window it was in at +//! registration time. +//! +//! To run: cargo run -p gpui --example move_entity_between_windows + +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::time::Duration; + +use gpui::{ + App, AppContext as _, Bounds, Context, EventEmitter, MouseButton, Render, SharedString, + Subscription, Task, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +struct MoveToNewWindow; + +struct HelloWorld { + text: SharedString, + tick_count: u32, + move_count: u32, + _tasks: Vec>, + _subscriptions: Vec, +} + +impl EventEmitter for HelloWorld {} + +impl HelloWorld { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let self_entity = cx.entity(); + + let task = cx.spawn_in(window, async move |this, cx| { + loop { + cx.background_executor().timer(Duration::from_secs(1)).await; + let result = this.update_in(cx, |this, window, _cx| { + this.tick_count += 1; + println!( + "tick #{} fired in entity's current window {}", + this.tick_count, + window.window_handle().window_id().as_u64(), + ); + }); + if let Err(err) = result { + println!("tick task giving up: {err}"); + return; + } + } + }); + + let subscription = cx.subscribe_in::<_, MoveToNewWindow>( + &self_entity, + window, + move |this, _emitter, _event, window, cx| { + let entered_window_id = window.window_handle().window_id().as_u64(); + println!( + "MoveToNewWindow handler fired in entity's current window {entered_window_id}", + ); + + this.move_count += 1; + cx.notify(); + + let entity = cx.entity(); + let old_window = window.window_handle(); + cx.defer(move |cx| { + let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + move |_, _| entity, + ) + .expect("failed to open new window"); + old_window + .update(cx, |_, window, _| window.remove_window()) + .ok(); + }); + }, + ); + + Self { + text: "World".into(), + tick_count: 0, + move_count: 0, + _tasks: vec![task], + _subscriptions: vec![subscription], + } + } +} + +impl Render for HelloWorld { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let window_id = window.window_handle().window_id().as_u64(); + + div() + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .text_xl() + .text_color(rgb(0xffffff)) + .child(format!("Hello, {}!", self.text)) + .child(format!("Rendering in window: {window_id}")) + .child(format!("Ticks observed by entity: {}", self.tick_count)) + .child(format!("Moves observed by entity: {}", self.move_count)) + .child( + div() + .px_4() + .py_2() + .bg(rgb(0x4040ff)) + .rounded_md() + .child("Move me to a new window") + .on_mouse_down( + MouseButton::Left, + cx.listener(|_this, _, _window, cx| { + cx.emit(MoveToNewWindow); + }), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| HelloWorld::new(window, cx)), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/on_window_close_quit.rs b/vendor/gpui-pre/examples/on_window_close_quit.rs new file mode 100644 index 0000000..d61e23e --- /dev/null +++ b/vendor/gpui-pre/examples/on_window_close_quit.rs @@ -0,0 +1,103 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, FocusHandle, KeyBinding, Window, WindowBounds, WindowOptions, actions, + div, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +actions!(example, [CloseWindow]); + +struct ExampleWindow { + focus_handle: FocusHandle, +} + +impl Render for ExampleWindow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .on_action(|_: &CloseWindow, window, _| { + window.remove_window(); + }) + .track_focus(&self.focus_handle) + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child( + "Closing this window with cmd-w or the traffic lights should quit the application!", + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + + cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]); + cx.on_window_closed(|cx, _window_id| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + cx.activate(false); + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + ExampleWindow { focus_handle } + }) + }, + ) + .unwrap(); + + bounds.origin.x += bounds.size.width; + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + ExampleWindow { focus_handle } + }) + }, + ) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/opacity.rs b/vendor/gpui-pre/examples/opacity.rs new file mode 100644 index 0000000..c2af342 --- /dev/null +++ b/vendor/gpui-pre/examples/opacity.rs @@ -0,0 +1,195 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::{fs, path::PathBuf}; + +use anyhow::Result; +use gpui::{ + App, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task, Window, + WindowBounds, WindowOptions, div, hsla, img, prelude::*, px, rgb, size, svg, +}; +use gpui_platform::application; + +struct Assets { + base: PathBuf, +} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + fs::read(self.base.join(path)) + .map(|data| Some(std::borrow::Cow::Owned(data))) + .map_err(|e| e.into()) + } + + fn list(&self, path: &str) -> Result> { + fs::read_dir(self.base.join(path)) + .map(|entries| { + entries + .filter_map(|entry| { + entry + .ok() + .and_then(|entry| entry.file_name().into_string().ok()) + .map(SharedString::from) + }) + .collect() + }) + .map_err(|e| e.into()) + } +} + +struct HelloWorld { + _task: Option>, + opacity: f32, + animating: bool, +} + +impl HelloWorld { + fn new(_window: &mut Window, _: &mut Context) -> Self { + Self { + _task: None, + opacity: 0.5, + animating: false, + } + } + + fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { + self.opacity = 0.0; + self.animating = true; + cx.notify(); + } +} + +impl Render for HelloWorld { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self.animating { + self.opacity += 0.005; + if self.opacity >= 1.0 { + self.animating = false; + self.opacity = 1.0; + } else { + window.request_animation_frame(); + } + } + + div() + .flex() + .flex_row() + .size_full() + .bg(rgb(0xe0e0e0)) + .text_xl() + .child( + div() + .flex() + .size_full() + .justify_center() + .items_center() + .border_1() + .text_color(gpui::blue()) + .child(div().child("This is background text.")), + ) + .child( + div() + .id("panel") + .on_click(cx.listener(Self::start_animation)) + .absolute() + .top_8() + .left_8() + .right_8() + .bottom_8() + .opacity(self.opacity) + .flex() + .justify_center() + .items_center() + .bg(gpui::white()) + .border_3() + .border_color(gpui::red()) + .text_color(gpui::yellow()) + .child( + div() + .flex() + .flex_col() + .gap_2() + .justify_center() + .items_center() + .size(px(300.)) + .bg(gpui::blue()) + .border_3() + .border_color(gpui::black()) + .shadow(vec![ + BoxShadow::new(px(10.0), px(10.0), hsla(0.0, 0.0, 0.0, 0.5)) + .blur_radius(px(1.0)) + .spread_radius(px(5.0)), + ]) + .child(img("image/app-icon.png").size_8()) + .child("Opacity Panel (Click to test)") + .child( + div() + .id("deep-level-text") + .flex() + .justify_center() + .items_center() + .p_4() + .bg(gpui::black()) + .text_color(gpui::white()) + .text_decoration_2() + .text_decoration_wavy() + .text_decoration_color(gpui::red()) + .child(format!("opacity: {:.1}", self.opacity)), + ) + .child( + svg() + .path("image/arrow_circle.svg") + .text_color(gpui::black()) + .text_2xl() + .size_8(), + ) + .child( + div() + .flex() + .children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| { + div() + .child(emoji.to_string()) + .hover(|style| style.opacity(0.5)) + })), + ) + .child(img("image/black-cat-typing.gif").size_12()), + ), + ) + } +} + +fn run_example() { + application() + .with_assets(Assets { + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), + }) + .run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| HelloWorld::new(window, cx)), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/ownership_post.rs b/vendor/gpui-pre/examples/ownership_post.rs new file mode 100644 index 0000000..04a0b6f --- /dev/null +++ b/vendor/gpui-pre/examples/ownership_post.rs @@ -0,0 +1,56 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{App, Context, Entity, EventEmitter, prelude::*}; +use gpui_platform::application; + +struct Counter { + count: usize, +} + +struct Change { + increment: usize, +} + +impl EventEmitter for Counter {} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let counter: Entity = cx.new(|_cx| Counter { count: 0 }); + let subscriber = cx.new(|cx: &mut Context| { + cx.subscribe(&counter, |subscriber, _emitter, event, _cx| { + subscriber.count += event.increment * 2; + }) + .detach(); + + Counter { + count: counter.read(cx).count * 2, + } + }); + + counter.update(cx, |counter, cx| { + counter.count += 2; + cx.notify(); + cx.emit(Change { increment: 2 }); + }); + + assert_eq!(subscriber.read(cx).count, 4); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/painting.rs b/vendor/gpui-pre/examples/painting.rs new file mode 100644 index 0000000..3d89b0f --- /dev/null +++ b/vendor/gpui-pre/examples/painting.rs @@ -0,0 +1,478 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels, + Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, linear_color_stop, + linear_gradient, point, prelude::*, px, quad, rgb, size, +}; +use gpui_platform::application; + +struct PaintingViewer { + default_lines: Vec<(Path, Background)>, + background_quads: Vec<(Bounds, Background)>, + lines: Vec>>, + start: Point, + dashed: bool, + _painting: bool, +} + +impl PaintingViewer { + fn new(_window: &mut Window, _cx: &mut Context) -> Self { + let mut lines = vec![]; + + // Black squares beneath transparent paths. + let background_quads = vec![ + ( + Bounds { + origin: point(px(70.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(170.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(270.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(370.), px(70.)), + size: size(px(40.), px(40.)), + }, + gpui::black().into(), + ), + ( + Bounds { + origin: point(px(450.), px(50.)), + size: size(px(80.), px(80.)), + }, + gpui::black().into(), + ), + ]; + + // 50% opaque red path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(50.), px(50.))); + builder.line_to(point(px(130.), px(50.))); + builder.line_to(point(px(130.), px(130.))); + builder.line_to(point(px(50.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let red = rgb(0xFF0000).alpha(0.5); + lines.push((path, red.into())); + + // 50% opaque blue path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(150.), px(50.))); + builder.line_to(point(px(230.), px(50.))); + builder.line_to(point(px(230.), px(130.))); + builder.line_to(point(px(150.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let blue = rgb(0x0000FF).alpha(0.5); + lines.push((path, blue.into())); + + // 50% opaque green path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(250.), px(50.))); + builder.line_to(point(px(330.), px(50.))); + builder.line_to(point(px(330.), px(130.))); + builder.line_to(point(px(250.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let green = rgb(0x00FF00).alpha(0.5); + lines.push((path, green.into())); + + // 50% opaque black path that extends across black quad. + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(50.))); + builder.line_to(point(px(430.), px(50.))); + builder.line_to(point(px(430.), px(130.))); + builder.line_to(point(px(350.), px(130.))); + builder.close(); + let path = builder.build().unwrap(); + let black = rgb(0x000000).alpha(0.5); + lines.push((path, black.into())); + + // Two 50% opaque red circles overlapping - center should be darker red + let mut builder = PathBuilder::fill(); + let center = point(px(530.), px(85.)); + let radius = px(30.); + builder.move_to(point(center.x + radius, center.y)); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x - radius, center.y), + ); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x + radius, center.y), + ); + builder.close(); + let path = builder.build().unwrap(); + let red1 = rgb(0xFF0000).alpha(0.5); + lines.push((path, red1.into())); + + let mut builder = PathBuilder::fill(); + let center = point(px(570.), px(85.)); + let radius = px(30.); + builder.move_to(point(center.x + radius, center.y)); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x - radius, center.y), + ); + builder.arc_to( + point(radius, radius), + px(0.), + false, + false, + point(center.x + radius, center.y), + ); + builder.close(); + let path = builder.build().unwrap(); + let red2 = rgb(0xFF0000).alpha(0.5); + lines.push((path, red2.into())); + + // draw a Rust logo + let mut builder = lyon::path::Path::svg_builder(); + lyon::extra::rust_logo::build_logo_path(&mut builder); + // move down the Path + let mut builder: PathBuilder = builder.into(); + builder.translate(point(px(10.), px(200.))); + builder.scale(0.9); + let path = builder.build().unwrap(); + lines.push((path, gpui::black().into())); + + // draw a lightening bolt ⚡ + let mut builder = PathBuilder::fill(); + builder.add_polygon( + &[ + point(px(150.), px(300.)), + point(px(200.), px(225.)), + point(px(200.), px(275.)), + point(px(250.), px(200.)), + ], + false, + ); + let path = builder.build().unwrap(); + lines.push((path, rgb(0x1d4ed8).into())); + + // draw a ⭐ + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(200.))); + builder.line_to(point(px(370.), px(260.))); + builder.line_to(point(px(430.), px(260.))); + builder.line_to(point(px(380.), px(300.))); + builder.line_to(point(px(400.), px(360.))); + builder.line_to(point(px(350.), px(320.))); + builder.line_to(point(px(300.), px(360.))); + builder.line_to(point(px(320.), px(300.))); + builder.line_to(point(px(270.), px(260.))); + builder.line_to(point(px(330.), px(260.))); + builder.line_to(point(px(350.), px(200.))); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(rgb(0xFACC15), 0.7), + linear_color_stop(rgb(0xD56D0C), 1.), + ) + .color_space(ColorSpace::Oklab), + )); + + // draw linear gradient + let square_bounds = Bounds { + origin: point(px(450.), px(200.)), + size: size(px(200.), px(80.)), + }; + let height = square_bounds.size.height; + let horizontal_offset = height; + let vertical_offset = px(30.); + let mut builder = PathBuilder::fill(); + builder.move_to(square_bounds.bottom_left()); + builder.curve_to( + square_bounds.origin + point(horizontal_offset, vertical_offset), + square_bounds.origin + point(px(0.0), vertical_offset), + ); + builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); + builder.curve_to( + square_bounds.bottom_right(), + square_bounds.top_right() + point(px(0.0), vertical_offset), + ); + builder.line_to(square_bounds.bottom_left()); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(gpui::blue(), 0.4), + linear_color_stop(gpui::red(), 1.), + ), + )); + + // draw a pie chart + let center = point(px(96.), px(96.)); + let pie_center = point(px(775.), px(255.)); + let segments = [ + ( + point(px(871.), px(255.)), + point(px(747.), px(163.)), + rgb(0x1374e9), + ), + ( + point(px(747.), px(163.)), + point(px(679.), px(263.)), + rgb(0xe13527), + ), + ( + point(px(679.), px(263.)), + point(px(754.), px(349.)), + rgb(0x0751ce), + ), + ( + point(px(754.), px(349.)), + point(px(854.), px(310.)), + rgb(0x209742), + ), + ( + point(px(854.), px(310.)), + point(px(871.), px(255.)), + rgb(0xfbc10a), + ), + ]; + + for (start, end, color) in segments { + let mut builder = PathBuilder::fill(); + builder.move_to(start); + builder.arc_to(center, px(0.), false, false, end); + builder.line_to(pie_center); + builder.close(); + let path = builder.build().unwrap(); + lines.push((path, color.into())); + } + + // draw a wave + let options = StrokeOptions::default() + .with_line_width(1.) + .with_line_join(lyon::path::LineJoin::Bevel); + let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); + builder.move_to(point(px(40.), px(420.))); + for i in 1..50 { + builder.line_to(point( + px(40.0 + i as f32 * 10.0), + px(420.0 + (i as f32 * 10.0).sin() * 40.0), + )); + } + let path = builder.build().unwrap(); + lines.push((path, gpui::green().into())); + + Self { + default_lines: lines.clone(), + background_quads, + lines: vec![], + start: point(px(0.), px(0.)), + dashed: false, + _painting: false, + } + } + + fn clear(&mut self, cx: &mut Context) { + self.lines.clear(); + cx.notify(); + } +} + +fn button( + text: &str, + cx: &mut Context, + on_click: impl Fn(&mut PaintingViewer, &mut Context) + 'static, +) -> impl IntoElement { + div() + .id(text.to_string()) + .child(text.to_string()) + .bg(gpui::black()) + .text_color(gpui::white()) + .active(|this| this.opacity(0.8)) + .flex() + .px_3() + .py_1() + .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) +} + +impl Render for PaintingViewer { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let default_lines = self.default_lines.clone(); + let background_quads = self.background_quads.clone(); + let lines = self.lines.clone(); + let dashed = self.dashed; + + div() + .bg(gpui::white()) + .size_full() + .p_4() + .flex() + .flex_col() + .child( + div() + .flex() + .gap_2() + .justify_between() + .items_center() + .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") + .child( + div() + .flex() + .gap_x_2() + .child(button( + if dashed { "Solid" } else { "Dashed" }, + cx, + move |this, _| this.dashed = !dashed, + )) + .child(button("Clear", cx, |this, cx| this.clear(cx))), + ), + ) + .child( + div() + .size_full() + .child( + canvas( + move |_, _, _| {}, + move |_, _, window, _| { + // First draw background quads + for (bounds, color) in background_quads.iter() { + window.paint_quad(quad( + *bounds, + px(0.), + *color, + px(0.), + gpui::transparent_black(), + Default::default(), + )); + } + + // Then draw the default paths on top + for (path, color) in default_lines { + window.paint_path(path, color); + } + + for points in lines { + if points.len() < 2 { + continue; + } + + let mut builder = PathBuilder::stroke(px(1.)); + if dashed { + builder = builder.dash_array(&[px(4.), px(2.)]); + } + for (i, p) in points.into_iter().enumerate() { + if i == 0 { + builder.move_to(p); + } else { + builder.line_to(p); + } + } + + if let Ok(path) = builder.build() { + window.paint_path(path, gpui::black()); + } + } + }, + ) + .size_full(), + ) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|this, ev: &MouseDownEvent, _, _| { + this._painting = true; + this.start = ev.position; + let path = vec![ev.position]; + this.lines.push(path); + }), + ) + .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| { + if !this._painting { + return; + } + + let is_shifted = ev.modifiers.shift; + let mut pos = ev.position; + // When holding shift, draw a straight line + if is_shifted { + let dx = pos.x - this.start.x; + let dy = pos.y - this.start.y; + if dx.abs() > dy.abs() { + pos.y = this.start.y; + } else { + pos.x = this.start.x; + } + } + + if let Some(path) = this.lines.last_mut() { + path.push(pos); + } + + cx.notify(); + })) + .on_mouse_up( + gpui::MouseButton::Left, + cx.listener(|this, _, _, _| { + this._painting = false; + }), + ), + ) + } +} + +fn run_example() { + application().run(|cx| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window( + WindowOptions { + focus: true, + ..Default::default() + }, + |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), + ) + .unwrap(); + cx.on_window_closed(|cx, _window_id| { + cx.quit(); + }) + .detach(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/paths_bench.rs b/vendor/gpui-pre/examples/paths_bench.rs new file mode 100644 index 0000000..236e70e --- /dev/null +++ b/vendor/gpui-pre/examples/paths_bench.rs @@ -0,0 +1,113 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + Background, Bounds, ColorSpace, Context, Path, PathBuilder, Pixels, Render, TitlebarOptions, + Window, WindowBounds, WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, + prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0); +const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0); + +struct PaintingViewer { + default_lines: Vec<(Path, Background)>, + _painting: bool, +} + +impl PaintingViewer { + fn new(_window: &mut Window, _cx: &mut Context) -> Self { + let mut lines = vec![]; + + // draw a lightening bolt ⚡ + for _ in 0..2000 { + // draw a ⭐ + let mut builder = PathBuilder::fill(); + builder.move_to(point(px(350.), px(100.))); + builder.line_to(point(px(370.), px(160.))); + builder.line_to(point(px(430.), px(160.))); + builder.line_to(point(px(380.), px(200.))); + builder.line_to(point(px(400.), px(260.))); + builder.line_to(point(px(350.), px(220.))); + builder.line_to(point(px(300.), px(260.))); + builder.line_to(point(px(320.), px(200.))); + builder.line_to(point(px(270.), px(160.))); + builder.line_to(point(px(330.), px(160.))); + builder.line_to(point(px(350.), px(100.))); + let path = builder.build().unwrap(); + lines.push(( + path, + linear_gradient( + 180., + linear_color_stop(rgb(0xFACC15), 0.7), + linear_color_stop(rgb(0xD56D0C), 1.), + ) + .color_space(ColorSpace::Oklab), + )); + } + + Self { + default_lines: lines, + _painting: false, + } + } +} + +impl Render for PaintingViewer { + fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { + window.request_animation_frame(); + let lines = self.default_lines.clone(); + div().size_full().child( + canvas( + move |_, _, _| {}, + move |_, _, window, _| { + for (path, color) in lines { + window.paint_path(path, color); + } + }, + ) + .size_full(), + ) + } +} + +fn run_example() { + application().run(|cx| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window( + WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some("Vulkan".into()), + ..Default::default() + }), + focus: true, + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT), + cx, + ))), + ..Default::default() + }, + |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/pattern.rs b/vendor/gpui-pre/examples/pattern.rs new file mode 100644 index 0000000..7d2e98d --- /dev/null +++ b/vendor/gpui-pre/examples/pattern.rs @@ -0,0 +1,136 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, AppContext, Bounds, Context, Window, WindowBounds, WindowOptions, div, linear_color_stop, + linear_gradient, pattern_slash, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +struct PatternExample; + +impl Render for PatternExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .bg(rgb(0xffffff)) + .size(px(600.0)) + .justify_center() + .items_center() + .shadow_lg() + .text_xl() + .text_color(rgb(0x000000)) + .child("Pattern Example") + .child( + div() + .flex() + .flex_col() + .border_1() + .border_color(gpui::blue()) + .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( + gpui::red(), + 18.0 / 4.0, + 18.0 / 4.0, + ))) + .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( + gpui::red(), + 18.0 / 4.0, + 18.0 / 4.0, + ))) + .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( + gpui::red(), + 18.0 / 4.0, + 18.0 / 4.0, + ))) + .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash( + gpui::red(), + 18.0 / 4.0, + 18.0 / 2.0, + ))), + ) + .child( + div() + .flex() + .flex_col() + .border_1() + .border_color(gpui::blue()) + .bg(gpui::green().opacity(0.16)) + .child("Elements the same height should align") + .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( + gpui::red(), + 56.0 / 6.0, + 56.0 / 6.0, + ))) + .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( + gpui::green(), + 56.0 / 6.0, + 56.0 / 6.0, + ))) + .child(div().w(px(256.0)).h(px(56.0)).bg(pattern_slash( + gpui::blue(), + 56.0 / 6.0, + 56.0 / 6.0, + ))) + .child(div().w(px(256.0)).h(px(26.0)).bg(pattern_slash( + gpui::yellow(), + 56.0 / 6.0, + 56.0 / 6.0, + ))), + ) + .child( + div() + .border_1() + .border_color(gpui::blue()) + .w(px(240.0)) + .h(px(40.0)) + .bg(gpui::red()), + ) + .child( + div() + .border_1() + .border_color(gpui::blue()) + .w(px(240.0)) + .h(px(40.0)) + .bg(linear_gradient( + 45., + linear_color_stop(gpui::red(), 0.), + linear_color_stop(gpui::blue(), 1.), + )), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_window, cx| cx.new(|_cx| PatternExample), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/popover.rs b/vendor/gpui-pre/examples/popover.rs new file mode 100644 index 0000000..7678a6a --- /dev/null +++ b/vendor/gpui-pre/examples/popover.rs @@ -0,0 +1,197 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + Anchor, App, Context, Div, Hsla, Stateful, Window, WindowOptions, anchored, deferred, div, + prelude::*, px, +}; +use gpui_platform::application; + +/// An example show use deferred to create a floating layers. +struct HelloWorld { + open: bool, + secondary_open: bool, +} + +fn button(id: &'static str) -> Stateful
{ + div() + .id(id) + .bg(gpui::black()) + .text_color(gpui::white()) + .px_3() + .py_1() +} + +fn popover() -> Div { + div() + .flex() + .flex_col() + .items_center() + .justify_center() + .shadow_lg() + .p_3() + .rounded_md() + .bg(gpui::white()) + .text_color(gpui::black()) + .border_1() + .text_sm() + .border_color(gpui::black().opacity(0.1)) +} + +fn line(color: Hsla) -> Div { + div().w(px(480.)).h_2().bg(color.opacity(0.25)) +} + +impl HelloWorld { + fn render_secondary_popover( + &mut self, + _window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + button("secondary-btn") + .mt_2() + .child("Child Popover") + .on_click(cx.listener(|this, _, _, cx| { + this.secondary_open = true; + cx.notify(); + })) + .when(self.secondary_open, |this| { + this.child( + // Now GPUI supports nested deferred! + deferred( + anchored() + .anchor(Anchor::TopLeft) + .snap_to_window_with_margin(px(8.)) + .child( + popover() + .child("This is second level Popover with nested deferred!") + .bg(gpui::white()) + .border_color(gpui::blue()) + .on_mouse_down_out(cx.listener(|this, _, _, cx| { + this.secondary_open = false; + cx.notify(); + })), + ), + ) + .priority(2), + ) + }) + } +} + +impl Render for HelloWorld { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .size_full() + .bg(gpui::white()) + .text_color(gpui::black()) + .justify_center() + .items_center() + .child( + div() + .flex() + .flex_row() + .gap_4() + .child( + button("popover0").child("Opened Popover").child( + deferred( + anchored() + .anchor(Anchor::TopLeft) + .snap_to_window_with_margin(px(8.)) + .child(popover().w_96().gap_3().child( + "This is a default opened Popover, \ + we can use deferred to render it \ + in a floating layer.", + )), + ) + .priority(0), + ), + ) + .child( + button("popover1") + .child("Open Popover") + .on_click(cx.listener(|this, _, _, cx| { + this.open = true; + cx.notify(); + })) + .when(self.open, |this| { + this.child( + deferred( + anchored() + .anchor(Anchor::TopLeft) + .snap_to_window_with_margin(px(8.)) + .child( + popover() + .w_96() + .gap_3() + .child( + "This is first level Popover, \ + we can use deferred to render it \ + in a floating layer.\n\ + Click outside to close.", + ) + .when(!self.secondary_open, |this| { + this.on_mouse_down_out(cx.listener( + |this, _, _, cx| { + this.open = false; + cx.notify(); + }, + )) + }) + // Here we need render popover after the content + // to ensure it will be on top layer. + .child( + self.render_secondary_popover(window, cx), + ), + ), + ) + .priority(1), + ) + }), + ), + ) + .child( + "Here is an example text rendered, \ + to ensure the Popover will float above this contents.", + ) + .children([ + line(gpui::red()), + line(gpui::yellow()), + line(gpui::blue()), + line(gpui::green()), + ]) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.open_window(WindowOptions::default(), |_, cx| { + cx.new(|_| HelloWorld { + open: false, + secondary_open: false, + }) + }) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/scrollable.rs b/vendor/gpui-pre/examples/scrollable.rs new file mode 100644 index 0000000..c817956 --- /dev/null +++ b/vendor/gpui-pre/examples/scrollable.rs @@ -0,0 +1,78 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size}; +use gpui_platform::application; + +struct Scrollable {} + +impl Render for Scrollable { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .id("vertical") + .p_4() + .overflow_scroll() + .bg(gpui::white()) + .child("Example for test 2 way scroll in nested layout") + .child( + div() + .h(px(5000.)) + .border_1() + .border_color(gpui::blue()) + .bg(gpui::blue().opacity(0.05)) + .p_4() + .child( + div() + .mb_5() + .w_full() + .id("horizontal") + .overflow_scroll() + .child( + div() + .w(px(2000.)) + .h(px(150.)) + .bg(gpui::green().opacity(0.1)) + .hover(|this| this.bg(gpui::green().opacity(0.2))) + .border_1() + .border_color(gpui::green()) + .p_4() + .child("Scroll Horizontal"), + ), + ) + .child("Scroll Vertical"), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Scrollable {}), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/set_menus.rs b/vendor/gpui-pre/examples/set_menus.rs new file mode 100644 index 0000000..508a60e --- /dev/null +++ b/vendor/gpui-pre/examples/set_menus.rs @@ -0,0 +1,134 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, WindowOptions, + actions, div, prelude::*, +}; +use gpui_platform::application; + +struct SetMenus; + +impl Render for SetMenus { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .bg(gpui::white()) + .size_full() + .justify_center() + .items_center() + .text_xl() + .text_color(gpui::black()) + .child("Set Menus Example") + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.set_global(AppState::new()); + + // Bring the menu bar to the foreground (so you can see the menu bar) + cx.activate(true); + // Register the `quit` function so it can be referenced + // by the `MenuItem::action` in the menu bar + cx.on_action(quit); + cx.on_action(toggle_check); + // Add menu items + set_app_menus(cx); + cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} + +#[derive(PartialEq)] +enum ViewMode { + List, + Grid, +} + +impl ViewMode { + fn toggle(&mut self) { + *self = match self { + ViewMode::List => ViewMode::Grid, + ViewMode::Grid => ViewMode::List, + } + } +} + +impl Into for ViewMode { + fn into(self) -> SharedString { + match self { + ViewMode::List => "List", + ViewMode::Grid => "Grid", + } + .into() + } +} + +struct AppState { + view_mode: ViewMode, +} + +impl AppState { + fn new() -> Self { + Self { + view_mode: ViewMode::List, + } + } +} + +impl Global for AppState {} + +fn set_app_menus(cx: &mut App) { + let app_state = cx.global::(); + cx.set_menus([Menu::new("set_menus").items([ + MenuItem::os_submenu("Services", SystemMenuType::Services), + MenuItem::separator(), + MenuItem::action("Disabled Item", gpui::NoAction).disabled(true), + MenuItem::submenu(Menu::new("Disabled Submenu").disabled(true)), + MenuItem::separator(), + MenuItem::action("List Mode", ToggleCheck).checked(app_state.view_mode == ViewMode::List), + MenuItem::submenu( + Menu::new("Mode").items([ + MenuItem::action(ViewMode::List, ToggleCheck) + .checked(app_state.view_mode == ViewMode::List), + MenuItem::action(ViewMode::Grid, ToggleCheck) + .checked(app_state.view_mode == ViewMode::Grid), + ]), + ), + MenuItem::separator(), + MenuItem::action("Quit", Quit), + ])]); +} + +// Associate actions using the `actions!` macro (or `Action` derive macro) +actions!(set_menus, [Quit, ToggleCheck]); + +// Define the quit function that is registered with the App +fn quit(_: &Quit, cx: &mut App) { + println!("Gracefully quitting the application..."); + cx.quit(); +} + +fn toggle_check(_: &ToggleCheck, cx: &mut App) { + let app_state = cx.global_mut::(); + app_state.view_mode.toggle(); + set_app_menus(cx); +} diff --git a/vendor/gpui-pre/examples/shadow.rs b/vendor/gpui-pre/examples/shadow.rs new file mode 100644 index 0000000..2d8dc09 --- /dev/null +++ b/vendor/gpui-pre/examples/shadow.rs @@ -0,0 +1,622 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, BoxShadow, Context, Div, SharedString, Window, WindowBounds, WindowOptions, div, + hsla, prelude::*, px, relative, rgb, size, +}; +use gpui_platform::application; + +struct Shadow {} + +impl Shadow { + fn base() -> Div { + div() + .size_16() + .bg(rgb(0xffffff)) + .rounded_full() + .border_1() + .border_color(hsla(0.0, 0.0, 0.0, 0.1)) + } + + fn square() -> Div { + div() + .size_16() + .bg(rgb(0xffffff)) + .border_1() + .border_color(hsla(0.0, 0.0, 0.0, 0.1)) + } + + fn rounded_small() -> Div { + div() + .size_16() + .bg(rgb(0xffffff)) + .rounded(px(4.)) + .border_1() + .border_color(hsla(0.0, 0.0, 0.0, 0.1)) + } + + fn rounded_medium() -> Div { + div() + .size_16() + .bg(rgb(0xffffff)) + .rounded(px(8.)) + .border_1() + .border_color(hsla(0.0, 0.0, 0.0, 0.1)) + } + + fn rounded_large() -> Div { + div() + .size_16() + .bg(rgb(0xffffff)) + .rounded(px(12.)) + .border_1() + .border_color(hsla(0.0, 0.0, 0.0, 0.1)) + } +} + +fn example(label: impl Into, example: impl IntoElement) -> impl IntoElement { + let label = label.into(); + + div() + .flex() + .flex_col() + .justify_center() + .items_center() + .w(relative(1. / 6.)) + .border_r_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .child( + div() + .flex() + .items_center() + .justify_center() + .flex_1() + .py_12() + .child(example), + ) + .child( + div() + .w_full() + .border_t_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .p_1() + .flex() + .items_center() + .child(label), + ) +} + +impl Render for Shadow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .id("shadow-example") + .overflow_y_scroll() + .bg(rgb(0xffffff)) + .size_full() + .text_xs() + .child(div().flex().flex_col().w_full().children(vec![ + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .flex_row() + .children(vec![ + example( + "Square", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded 4", + Shadow::rounded_small().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded 8", + Shadow::rounded_medium().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded 16", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Circle", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + ]), + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .w_full() + .children(vec![ + example("None", Shadow::base()), + // 2Xsmall shadow + example("2X Small", Shadow::base().shadow_2xs()), + // Xsmall shadow + example("Extra Small", Shadow::base().shadow_xs()), + // Small shadow + example("Small", Shadow::base().shadow_sm()), + // Medium shadow + example("Medium", Shadow::base().shadow_md()), + // Large shadow + example("Large", Shadow::base().shadow_lg()), + example("Extra Large", Shadow::base().shadow_xl()), + example("2X Large", Shadow::base().shadow_2xl()), + ]), + // Horizontal list of increasing blur radii + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Blur 0", + Shadow::base().shadow(vec![BoxShadow::new( + px(0.), + px(8.), + hsla(0.0, 0.0, 0.0, 0.3), + )]), + ), + example( + "Blur 2", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(2.)), + ]), + ), + example( + "Blur 4", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(4.)), + ]), + ), + example( + "Blur 8", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Blur 16", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(16.)), + ]), + ), + ]), + // Horizontal list of increasing spread radii + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Spread 0", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Spread 2", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + ]), + ), + example( + "Spread 4", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(4.)), + ]), + ), + example( + "Spread 8", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(8.)), + ]), + ), + example( + "Spread 16", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(16.)), + ]), + ), + ]), + // Square spread examples + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Square Spread 0", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Square Spread 8", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(8.)), + ]), + ), + example( + "Square Spread 16", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(16.)), + ]), + ), + ]), + // Rounded large spread examples + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Rounded Large Spread 0", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded Large Spread 8", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(8.)), + ]), + ), + example( + "Rounded Large Spread 16", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.3)) + .blur_radius(px(8.)) + .spread_radius(px(16.)), + ]), + ), + ]), + // Directional shadows + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Left", + Shadow::base().shadow(vec![ + BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Right", + Shadow::base().shadow(vec![ + BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Top", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Bottom", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + ]), + // Square directional shadows + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Square Left", + Shadow::square().shadow(vec![ + BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Square Right", + Shadow::square().shadow(vec![ + BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Square Top", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Square Bottom", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + ]), + // Rounded large directional shadows + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Rounded Large Left", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(-8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded Large Right", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(8.), px(0.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded Large Top", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(-8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + example( + "Rounded Large Bottom", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.5, 0.5, 0.3)) + .blur_radius(px(8.)), + ]), + ), + ]), + // Multiple shadows for different shapes + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .children(vec![ + example( + "Circle Multiple", + Shadow::base().shadow(vec![ + BoxShadow::new( + px(0.), + px(-12.), + hsla(0.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(12.), + px(0.), + hsla(60.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(0.), + px(12.), + hsla(120.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(-12.), + px(0.), + hsla(240.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + ]), + ), + example( + "Square Multiple", + Shadow::square().shadow(vec![ + BoxShadow::new( + px(0.), + px(-12.), + hsla(0.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(12.), + px(0.), + hsla(60.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(0.), + px(12.), + hsla(120.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(-12.), + px(0.), + hsla(240.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + ]), + ), + example( + "Rounded Large Multiple", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new( + px(0.), + px(-12.), + hsla(0.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(12.), + px(0.), + hsla(60.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(0.), + px(12.), + hsla(120.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + BoxShadow::new( + px(-12.), + px(0.), + hsla(240.0 / 360., 1.0, 0.5, 0.3), + ) + .blur_radius(px(8.)) + .spread_radius(px(2.)), + ]), + ), + ]), + // Inset shadows (CSS `box-shadow: inset ...`). + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .w_full() + .children(vec![ + example( + "Inset basic", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5)) + .blur_radius(px(12.)) + .inset(), + ]), + ), + example( + "Inset offset", + Shadow::base().shadow(vec![ + BoxShadow::new(px(6.), px(6.), hsla(0.0, 0.0, 0.0, 0.5)) + .blur_radius(px(8.)) + .inset(), + ]), + ), + example( + "Inset spread", + Shadow::base().shadow(vec![ + BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.5)) + .blur_radius(px(4.)) + .spread_radius(px(8.)) + .inset(), + ]), + ), + example( + "Inset rounded", + Shadow::rounded_large().shadow(vec![ + BoxShadow::new(px(0.), px(4.), hsla(0.0, 0.0, 0.0, 0.5)) + .blur_radius(px(10.)) + .spread_radius(px(2.)) + .inset(), + ]), + ), + example( + "Inset sharp", + Shadow::square().shadow(vec![ + BoxShadow::new(px(0.), px(0.), hsla(0.0, 0.0, 0.0, 0.6)) + .spread_radius(px(6.)) + .inset(), + ]), + ), + ]), + // Combined: drop + inset shadows on the same element. + div() + .border_b_1() + .border_color(hsla(0.0, 0.0, 0.0, 1.0)) + .flex() + .w_full() + .children(vec![example( + "Drop + Inset", + Shadow::rounded_medium().shadow(vec![ + BoxShadow::new(px(0.), px(8.), hsla(0.0, 0.0, 0.0, 0.25)) + .blur_radius(px(12.)), + BoxShadow::new(px(0.), px(2.), hsla(0.0, 0.0, 0.0, 0.4)) + .blur_radius(px(4.)) + .inset(), + ]), + )]), + ])) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Shadow {}), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/svg/dragon.svg b/vendor/gpui-pre/examples/svg/dragon.svg new file mode 100644 index 0000000..e2de1a0 --- /dev/null +++ b/vendor/gpui-pre/examples/svg/dragon.svg @@ -0,0 +1,240 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/gpui-pre/examples/svg/svg.rs b/vendor/gpui-pre/examples/svg/svg.rs new file mode 100644 index 0000000..c5ac9b4 --- /dev/null +++ b/vendor/gpui-pre/examples/svg/svg.rs @@ -0,0 +1,108 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "../example_support/fonts.rs"] +mod example_support; + +use std::fs; +use std::path::PathBuf; + +use anyhow::Result; +use gpui::{ + App, AssetSource, Bounds, Context, SharedString, Window, WindowBounds, WindowOptions, div, + prelude::*, px, rgb, size, svg, +}; +use gpui_platform::application; + +struct Assets { + base: PathBuf, +} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + fs::read(self.base.join(path)) + .map(|data| Some(std::borrow::Cow::Owned(data))) + .map_err(|err| err.into()) + } + + fn list(&self, path: &str) -> Result> { + fs::read_dir(self.base.join(path)) + .map(|entries| { + entries + .filter_map(|entry| { + entry + .ok() + .and_then(|entry| entry.file_name().into_string().ok()) + .map(SharedString::from) + }) + .collect() + }) + .map_err(|err| err.into()) + } +} + +struct SvgExample; + +impl Render for SvgExample { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_row() + .size_full() + .justify_center() + .items_center() + .gap_8() + .bg(rgb(0xffffff)) + .child( + svg() + .path("svg/dragon.svg") + .size_8() + .text_color(rgb(0xff0000)), + ) + .child( + svg() + .path("svg/dragon.svg") + .size_8() + .text_color(rgb(0x00ff00)), + ) + .child( + svg() + .path("svg/dragon.svg") + .size_8() + .text_color(rgb(0x0000ff)), + ) + } +} + +fn run_example() { + application() + .with_assets(Assets { + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), + }) + .run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| SvgExample), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/system_notifications.rs b/vendor/gpui-pre/examples/system_notifications.rs new file mode 100644 index 0000000..faf6919 --- /dev/null +++ b/vendor/gpui-pre/examples/system_notifications.rs @@ -0,0 +1,155 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +//! Demonstrates posting, replacing, dismissing, and responding to system notifications. + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Div, SharedString, Stateful, SystemNotification, + SystemNotificationAction, SystemNotificationResponse, Window, WindowBounds, WindowOptions, div, + prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +const NOTIFICATION_TAG: &str = "gpui-system-notification-example"; + +struct SystemNotificationExample { + revision: usize, + status: SharedString, +} + +impl Render for SystemNotificationExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .size_full() + .gap_4() + .p_8() + .bg(rgb(0x18181b)) + .text_color(rgb(0xf4f4f5)) + .child(div().text_2xl().child("GPUI system notifications")) + .child(div().text_sm().text_color(rgb(0xa1a1aa)).child( + "Post repeatedly to replace the notification with the same tag. Click the \ + notification body or an action button to send a response back to GPUI.", + )) + .child( + div() + .flex() + .gap_3() + .child(button("show", "Show or replace").on_click(cx.listener( + |this, _, _, cx| { + this.revision += 1; + let revision = this.revision; + cx.show_system_notification(SystemNotification { + tag: NOTIFICATION_TAG.into(), + title: format!("Example notification {revision}").into(), + body: "This notification was posted by the GPUI example.".into(), + actions: vec![ + SystemNotificationAction { + id: "open".into(), + label: "Open".into(), + }, + SystemNotificationAction { + id: "snooze".into(), + label: "Snooze".into(), + }, + ], + }); + this.status = format!("Posted notification revision {revision}").into(); + cx.notify(); + }, + ))) + .child( + button("dismiss", "Dismiss").on_click(cx.listener(|this, _, _, cx| { + cx.dismiss_system_notification(NOTIFICATION_TAG); + this.status = "Dismissed the notification".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .mt_2() + .p_4() + .rounded_md() + .bg(rgb(0x27272a)) + .child(self.status.clone()), + ) + .when(cfg!(target_os = "macos"), |this| { + this.child(div().mt_2().text_xs().text_color(rgb(0x71717a)).child( + "macOS only delivers notifications when this example runs from an app bundle.", + )) + }) + } +} + +fn button(id: &'static str, label: &'static str) -> Stateful
{ + div() + .id(id) + .px_4() + .py_2() + .rounded_md() + .bg(rgb(0x3f3f46)) + .hover(|style| style.bg(rgb(0x52525b))) + .active(|style| style.bg(rgb(0x71717a))) + .cursor_pointer() + .child(label) +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.set_app_identity("dev.zed.gpui.system-notifications", "GPUI Notifications"); + + let view = cx.new(|_| SystemNotificationExample { + revision: 0, + status: "No notification posted yet".into(), + }); + cx.on_system_notification_response({ + let view = view.clone(); + move |response, cx| { + let SystemNotificationResponse { tag, action_id } = response; + view.update(cx, |this, cx| { + this.status = match action_id { + Some(action_id) => { + format!("Received action '{action_id}' for tag '{tag}'").into() + } + None => format!("Notification body clicked for tag '{tag}'").into(), + }; + cx.notify(); + }); + } + }); + + let bounds = Bounds::centered(None, size(px(560.), px(360.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(gpui::TitlebarOptions { + title: Some("System Notifications Example".into()), + ..Default::default() + }), + ..Default::default() + }, + move |_, _| view, + ) + .expect("failed to open system notifications example window"); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/tab_stop.rs b/vendor/gpui-pre/examples/tab_stop.rs new file mode 100644 index 0000000..cc7c34a --- /dev/null +++ b/vendor/gpui-pre/examples/tab_stop.rs @@ -0,0 +1,220 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, Stateful, Window, + WindowBounds, WindowOptions, actions, div, prelude::*, px, size, +}; +use gpui_platform::application; + +actions!(example, [Tab, TabPrev]); + +struct Example { + focus_handle: FocusHandle, + items: Vec, + message: SharedString, +} + +impl Example { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let items = vec![ + cx.focus_handle().tab_index(1).tab_stop(true), + cx.focus_handle().tab_index(2).tab_stop(true), + cx.focus_handle().tab_index(3).tab_stop(true), + cx.focus_handle(), + cx.focus_handle().tab_index(2).tab_stop(true), + ]; + + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle, cx); + + Self { + focus_handle, + items, + message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."), + } + } + + fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + window.focus_next(cx); + self.message = SharedString::from("You have pressed `Tab`."); + } + + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context) { + window.focus_prev(cx); + self.message = SharedString::from("You have pressed `Shift-Tab`."); + } +} + +impl Render for Example { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn tab_stop_style(this: T) -> T { + this.border_3().border_color(gpui::blue()) + } + + fn button(id: impl Into) -> Stateful
{ + div() + .id(id) + .h_10() + .flex_1() + .flex() + .justify_center() + .items_center() + .border_1() + .border_color(gpui::black()) + .bg(gpui::black()) + .text_color(gpui::white()) + .focus(tab_stop_style) + .shadow_sm() + } + + div() + .id("app") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) + .size_full() + .flex() + .flex_col() + .p_4() + .gap_3() + .bg(gpui::white()) + .text_color(gpui::black()) + .child(self.message.clone()) + .children( + self.items + .clone() + .into_iter() + .enumerate() + .map(|(ix, item_handle)| { + div() + .id(("item", ix)) + .track_focus(&item_handle) + .h_10() + .w_full() + .flex() + .justify_center() + .items_center() + .border_1() + .border_color(gpui::black()) + .when( + item_handle.tab_stop && item_handle.is_focused(window), + tab_stop_style, + ) + .map(|this| match item_handle.tab_stop { + true => this + .hover(|this| this.bg(gpui::black().opacity(0.1))) + .child(format!("tab_index: {}", item_handle.tab_index)), + false => this.opacity(0.4).child("tab_stop: false"), + }) + }), + ) + .child( + div() + .flex() + .flex_row() + .gap_3() + .items_center() + .child( + button("el1") + .tab_index(4) + .child("Button 1") + .on_click(cx.listener(|this, _, _, cx| { + this.message = "You have clicked Button 1.".into(); + cx.notify(); + })), + ) + .child( + button("el2") + .tab_index(5) + .child("Button 2") + .on_click(cx.listener(|this, _, _, cx| { + this.message = "You have clicked Button 2.".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .id("group-1") + .tab_index(6) + .tab_group() + .tab_stop(false) + .child( + button("group-1-button-1") + .tab_index(1) + .child("Tab index [6, 1]"), + ) + .child( + button("group-1-button-2") + .tab_index(2) + .child("Tab index [6, 2]"), + ) + .child( + button("group-1-button-3") + .tab_index(3) + .child("Tab index [6, 3]"), + ), + ) + .child( + div() + .id("group-2") + .tab_index(7) + .tab_group() + .tab_stop(false) + .child( + button("group-2-button-1") + .tab_index(1) + .child("Tab index [7, 1]"), + ) + .child( + button("group-2-button-2") + .tab_index(2) + .child("Tab index [7, 2]"), + ) + .child( + button("group-2-button-3") + .tab_index(3) + .child("Tab index [7, 3]"), + ), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.bind_keys([ + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + ]); + + let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .unwrap(); + + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/testing.rs b/vendor/gpui-pre/examples/testing.rs new file mode 100644 index 0000000..76e3b77 --- /dev/null +++ b/vendor/gpui-pre/examples/testing.rs @@ -0,0 +1,559 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +//! Example demonstrating GPUI's testing infrastructure. +//! +//! When run normally, this displays an interactive counter window. +//! The tests below demonstrate various GPUI testing patterns. +//! +//! Run the app: cargo run -p gpui --example testing +//! Run tests: cargo test -p gpui --example testing --features test-support + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, FocusHandle, Focusable, Render, Task, Window, WindowBounds, + WindowOptions, actions, div, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +actions!(counter, [Increment, Decrement]); + +struct Counter { + count: i32, + focus_handle: FocusHandle, + _subscription: gpui::Subscription, +} + +/// Event emitted by Counter +struct CounterEvent; + +impl gpui::EventEmitter for Counter {} + +impl Counter { + fn new(cx: &mut Context) -> Self { + let subscription = cx.subscribe_self(|this: &mut Self, _event: &CounterEvent, _cx| { + this.count = 999; + }); + + Self { + count: 0, + focus_handle: cx.focus_handle(), + _subscription: subscription, + } + } + + fn increment(&mut self, _: &Increment, _window: &mut Window, cx: &mut Context) { + self.count += 1; + cx.notify(); + } + + fn decrement(&mut self, _: &Decrement, _window: &mut Window, cx: &mut Context) { + self.count -= 1; + cx.notify(); + } + + fn load(&self, cx: &mut Context) -> Task<()> { + cx.spawn(async move |this, cx| { + // Simulate loading data (e.g., from disk or network) + this.update(cx, |counter, _| { + counter.count = 100; + }) + .ok(); + }) + } + + fn reload(&self, cx: &mut Context) { + cx.spawn(async move |this, cx| { + // Simulate reloading data in the background + this.update(cx, |counter, _| { + counter.count += 50; + }) + .ok(); + }) + .detach(); + } +} + +impl Focusable for Counter { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Counter { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .id("counter") + .key_context("Counter") + .on_action(cx.listener(Self::increment)) + .on_action(cx.listener(Self::decrement)) + .track_focus(&self.focus_handle) + .flex() + .flex_col() + .gap_4() + .bg(rgb(0x1e1e2e)) + .size_full() + .justify_center() + .items_center() + .child( + div() + .text_3xl() + .text_color(rgb(0xcdd6f4)) + .child(format!("{}", self.count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("decrement") + .px_4() + .py_2() + .bg(rgb(0x313244)) + .hover(|s| s.bg(rgb(0x45475a))) + .rounded_md() + .cursor_pointer() + .text_color(rgb(0xcdd6f4)) + .on_click(cx.listener(|this, _, window, cx| { + this.decrement(&Decrement, window, cx) + })) + .child("−"), + ) + .child( + div() + .id("increment") + .px_4() + .py_2() + .bg(rgb(0x313244)) + .hover(|s| s.bg(rgb(0x45475a))) + .rounded_md() + .cursor_pointer() + .text_color(rgb(0xcdd6f4)) + .on_click(cx.listener(|this, _, window, cx| { + this.increment(&Increment, window, cx) + })) + .child("+"), + ), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("load") + .px_4() + .py_2() + .bg(rgb(0x313244)) + .hover(|s| s.bg(rgb(0x45475a))) + .rounded_md() + .cursor_pointer() + .text_color(rgb(0xcdd6f4)) + .on_click(cx.listener(|this, _, _, cx| { + this.load(cx).detach(); + })) + .child("Load"), + ) + .child( + div() + .id("reload") + .px_4() + .py_2() + .bg(rgb(0x313244)) + .hover(|s| s.bg(rgb(0x45475a))) + .rounded_md() + .cursor_pointer() + .text_color(rgb(0xcdd6f4)) + .on_click(cx.listener(|this, _, _, cx| { + this.reload(cx); + })) + .child("Reload"), + ), + ) + .child( + div() + .text_sm() + .text_color(rgb(0x6c7086)) + .child("Press ↑/↓ or click buttons"), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.bind_keys([ + gpui::KeyBinding::new("up", Increment, Some("Counter")), + gpui::KeyBinding::new("down", Decrement, Some("Counter")), + ]); + + let bounds = Bounds::centered(None, size(px(300.), px(200.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + let counter = cx.new(|cx| Counter::new(cx)); + counter.focus_handle(cx).focus(window, cx); + counter + }, + ) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, VisualTestContext}; + use rand::prelude::*; + + /// Here's a basic GPUI test. Just add the macro and take a TestAppContext as an argument! + /// + /// Note that synchronous side effects run immediately after your "update*" calls complete. + #[gpui::test] + fn basic_testing(cx: &mut TestAppContext) { + let counter = cx.new(|cx| Counter::new(cx)); + + counter.update(cx, |counter, _| { + counter.count = 42; + }); + + // Note that TestAppContext doesn't support `read(cx)` + let updated = counter.read_with(cx, |counter, _| counter.count); + assert_eq!(updated, 42); + + // Emit an event - the subscriber will run immediately after the update finishes + counter.update(cx, |_, cx| { + cx.emit(CounterEvent); + }); + + let count_after_update = counter.read_with(cx, |counter, _| counter.count); + assert_eq!( + count_after_update, 999, + "Side effects should run after update completes" + ); + } + + /// Tests which involve the window require you to construct a VisualTestContext. + /// Just like synchronous side effects, the window will be drawn after every "update*" + /// call, so you can test render-dependent behavior. + #[gpui::test] + fn test_counter_in_window(cx: &mut TestAppContext) { + let window = cx.update(|cx| { + cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) + .unwrap() + }); + + let mut cx = VisualTestContext::from_window(window.into(), cx); + let counter = window.root(&mut cx).unwrap(); + + // Action dispatch depends on the element tree to resolve which action handler + // to call, and this works exactly as you'd expect in a test. + let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone()); + cx.update(|window, cx| { + focus_handle.dispatch_action(&Increment, window, cx); + }); + + let count_after = counter.read_with(&cx, |counter, _| counter.count); + assert_eq!( + count_after, 1, + "Action dispatched via focus handle should increment" + ); + } + + /// GPUI tests can also be async, simply add the async keyword before the test. + /// Note that the test executor is single thread, so async side effects (including + /// background tasks) won't run until you explicitly yield control. + #[gpui::test] + async fn test_async_operations(cx: &mut TestAppContext) { + let counter = cx.new(|cx| Counter::new(cx)); + + // Tasks can be awaited directly + counter.update(cx, |counter, cx| counter.load(cx)).await; + + let count = counter.read_with(cx, |counter, _| counter.count); + assert_eq!(count, 100, "Load task should have set count to 100"); + + // But side effects don't run until you yield control + counter.update(cx, |counter, cx| counter.reload(cx)); + + let count = counter.read_with(cx, |counter, _| counter.count); + assert_eq!(count, 100, "Detached reload task shouldn't have run yet"); + + // This runs all pending tasks + cx.run_until_parked(); + + let count = counter.read_with(cx, |counter, _| counter.count); + assert_eq!(count, 150, "Reload task should have run after parking"); + } + + /// Note that the test executor panics if you await a future that waits on + /// something outside GPUI's control, like a reading a file or network IO. + /// You should mock external systems where possible, as this feature can be used + /// to detect potential deadlocks in your async code. + /// + /// However, if you want to disable this check use `allow_parking()` + #[gpui::test] + async fn test_allow_parking(cx: &mut TestAppContext) { + // Allow the thread to park + cx.executor().allow_parking(); + + // Simulate an external system (like a file system) with an OS thread + let (tx, rx) = futures::channel::oneshot::channel(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(5)); + tx.send(42).ok(); + }); + + // Without allow_parking(), this await would panic because GPUI's + // scheduler runs out of tasks while waiting for the external thread. + let result = rx.await.unwrap(); + assert_eq!(result, 42); + } + + /// GPUI also provides support for property testing, via the iterations flag + #[gpui::test(iterations = 10)] + fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) { + let window = cx.update(|cx| { + cx.open_window(Default::default(), |_, cx| cx.new(|cx| Counter::new(cx))) + .unwrap() + }); + let mut cx = VisualTestContext::from_window(window.into(), cx); + + let counter = cx.new(|cx| Counter::new(cx)); + + // Perform random increments/decrements + let mut expected = 0i32; + for _ in 0..100 { + if rng.random_bool(0.5) { + expected += 1; + counter.update_in(&mut cx, |counter, window, cx| { + counter.increment(&Increment, window, cx) + }); + } else { + expected -= 1; + counter.update_in(&mut cx, |counter, window, cx| { + counter.decrement(&Decrement, window, cx) + }); + } + } + + let actual = counter.read_with(&cx, |counter, _| counter.count); + assert_eq!( + actual, expected, + "Counter should match expected after random ops" + ); + } + + /// Now, all of those tests are good, but GPUI also provides strong support for testing distributed systems. + /// Let's setup a mock network and enhance the counter to send messages over it. + mod distributed_systems { + use std::sync::{Arc, Mutex}; + + /// The state of the mock network. + struct MockNetworkState { + ordering: Vec, + a_to_b: Vec, + b_to_a: Vec, + } + + /// A mock network that delivers messages between two peers. + #[derive(Clone)] + struct MockNetwork { + state: Arc>, + } + + impl MockNetwork { + fn new() -> Self { + Self { + state: Arc::new(Mutex::new(MockNetworkState { + ordering: Vec::new(), + a_to_b: Vec::new(), + b_to_a: Vec::new(), + })), + } + } + + fn a_client(&self) -> NetworkClient { + NetworkClient { + network: self.clone(), + is_a: true, + } + } + + fn b_client(&self) -> NetworkClient { + NetworkClient { + network: self.clone(), + is_a: false, + } + } + } + + /// A client handle for sending/receiving messages over the mock network. + #[derive(Clone)] + struct NetworkClient { + network: MockNetwork, + is_a: bool, + } + + // See, networking is easy! + impl NetworkClient { + fn send(&self, value: i32) { + let mut network = self.network.state.lock().unwrap(); + network.ordering.push(value); + if self.is_a { + network.b_to_a.push(value); + } else { + network.a_to_b.push(value); + } + } + + fn receive_all(&self) -> Vec { + let mut network = self.network.state.lock().unwrap(); + if self.is_a { + network.a_to_b.drain(..).collect() + } else { + network.b_to_a.drain(..).collect() + } + } + } + + use gpui::Context; + + /// A networked counter that can send/receive over a mock network. + struct NetworkedCounter { + count: i32, + client: NetworkClient, + } + + impl NetworkedCounter { + fn new(client: NetworkClient) -> Self { + Self { count: 0, client } + } + + /// Increment the counter and broadcast the change. + fn increment(&mut self, delta: i32, cx: &mut Context) { + self.count += delta; + + cx.background_spawn({ + let client = self.client.clone(); + async move { + client.send(delta); + } + }) + .detach(); + } + + /// Process incoming increment requests. + fn sync(&mut self) { + for delta in self.client.receive_all() { + self.count += delta; + } + } + } + + use super::*; + + /// You can simulate distributed systems with multiple app contexts, simply by adding + /// additional parameters. + #[gpui::test] + fn test_app_sync(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let network = MockNetwork::new(); + + let a = cx_a.new(|_| NetworkedCounter::new(network.a_client())); + let b = cx_b.new(|_| NetworkedCounter::new(network.b_client())); + + // B increments locally and broadcasts the delta + b.update(cx_b, |b, cx| b.increment(42, cx)); + b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // B's count is set immediately + a.read_with(cx_a, |a, _| assert_eq!(a.count, 0)); // A's count is in a side effect + + cx_b.run_until_parked(); // Send the delta from B + a.update(cx_a, |a, _| a.sync()); // Receive the delta at A + + b.read_with(cx_b, |b, _| assert_eq!(b.count, 42)); // Both counts now match + a.read_with(cx_a, |a, _| assert_eq!(a.count, 42)); + } + + /// Multiple apps can run concurrently, and to capture this each test app shares + /// a dispatcher. Whenever you call `run_until_parked`, the dispatcher will randomly + /// pick which app's tasks to run next. This allows you to test that your distributed code + /// is robust to different execution orderings. + #[gpui::test(iterations = 10)] + fn test_random_interleaving( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + mut rng: StdRng, + ) { + let network = MockNetwork::new(); + + // Track execution order + let mut original_order = Vec::new(); + let a = cx_a.new(|_| NetworkedCounter::new(MockNetwork::a_client(&network))); + let b = cx_b.new(|_| NetworkedCounter::new(MockNetwork::b_client(&network))); + + let num_operations: usize = rng.random_range(3..8); + + for i in 0..num_operations { + let i = i as i32; + let which = rng.random_bool(0.5); + + original_order.push(i); + if which { + b.update(cx_b, |b, cx| b.increment(i, cx)); + } else { + a.update(cx_a, |a, cx| a.increment(i, cx)); + } + } + + // This will send all of the pending increment messages, from both a and b + cx_a.run_until_parked(); + + a.update(cx_a, |a, _| a.sync()); + b.update(cx_b, |b, _| b.sync()); + + let a_count = a.read_with(cx_a, |a, _| a.count); + let b_count = b.read_with(cx_b, |b, _| b.count); + + assert_eq!(a_count, b_count, "A and B should have the same count"); + + // Nicely format the execution order output. + // Run this test with `-- --nocapture` to see it! + let actual = network.state.lock().unwrap().ordering.clone(); + let spawned: Vec<_> = original_order.iter().map(|n| format!("{}", n)).collect(); + let ran: Vec<_> = actual.iter().map(|n| format!("{}", n)).collect(); + let diff: Vec<_> = original_order + .iter() + .zip(actual.iter()) + .map(|(o, a)| { + if o == a { + " ".to_string() + } else { + "^".to_string() + } + }) + .collect(); + println!("spawned: [{}]", spawned.join(", ")); + println!("ran: [{}]", ran.join(", ")); + println!(" [{}]", diff.join(", ")); + } + } +} diff --git a/vendor/gpui-pre/examples/text.rs b/vendor/gpui-pre/examples/text.rs new file mode 100644 index 0000000..7fa2a15 --- /dev/null +++ b/vendor/gpui-pre/examples/text.rs @@ -0,0 +1,432 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::Arc, +}; + +use gpui::{ + AbsoluteLength, App, Context, DefiniteLength, ElementId, Global, Hsla, Menu, SharedString, + TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds, colors::DefaultColors, + div, point, prelude::*, px, relative, rgb, size, +}; +use gpui_platform::application; +use std::iter; + +#[derive(Clone, Debug)] +pub struct TextContext { + font_size: f32, + line_height: f32, + type_scale: f32, +} + +impl Default for TextContext { + fn default() -> Self { + TextContext { + font_size: 16.0, + line_height: 1.3, + type_scale: 1.33, + } + } +} + +impl TextContext { + pub fn get_global(cx: &App) -> &Arc { + &cx.global::().0 + } +} + +#[derive(Clone, Debug)] +pub struct GlobalTextContext(pub Arc); + +impl Deref for GlobalTextContext { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GlobalTextContext { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Global for GlobalTextContext {} + +pub trait ActiveTextContext { + fn text_context(&self) -> &Arc; +} + +impl ActiveTextContext for App { + fn text_context(&self) -> &Arc { + &self.global::().0 + } +} + +#[derive(Clone, PartialEq)] +pub struct SpecimenTheme { + pub bg: Hsla, + pub fg: Hsla, +} + +impl Default for SpecimenTheme { + fn default() -> Self { + Self { + bg: gpui::white(), + fg: gpui::black(), + } + } +} + +impl SpecimenTheme { + pub fn invert(&self) -> Self { + Self { + bg: self.fg, + fg: self.bg, + } + } +} + +#[derive(Debug, Clone, PartialEq, IntoElement)] +struct Specimen { + id: ElementId, + scale: f32, + text_style: Option, + string: SharedString, + invert: bool, +} + +impl Specimen { + pub fn new(id: usize) -> Self { + let string = SharedString::new_static("The quick brown fox jumps over the lazy dog"); + let id_string = format!("specimen-{}", id); + let id = ElementId::Name(id_string.into()); + Self { + id, + scale: 1.0, + text_style: None, + string, + invert: false, + } + } + + pub fn invert(mut self) -> Self { + self.invert = !self.invert; + self + } + + pub fn scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } +} + +impl RenderOnce for Specimen { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let rem_size = window.rem_size(); + let scale = self.scale; + let global_style = cx.text_context(); + + let style_override = self.text_style; + + let mut font_size = global_style.font_size; + let mut line_height = global_style.line_height; + + if let Some(style_override) = style_override { + font_size = style_override.font_size.to_pixels(rem_size).into(); + line_height = match style_override.line_height { + DefiniteLength::Absolute(absolute_len) => match absolute_len { + AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(), + AbsoluteLength::Pixels(absolute_len) => absolute_len.into(), + }, + DefiniteLength::Fraction(value) => value, + }; + } + + let mut theme = SpecimenTheme::default(); + + if self.invert { + theme = theme.invert(); + } + + div() + .id(self.id) + .bg(theme.bg) + .text_color(theme.fg) + .text_size(px(font_size * scale)) + .line_height(relative(line_height)) + .p(px(10.0)) + .child(self.string) + } +} + +#[derive(Debug, Clone, PartialEq, IntoElement)] +struct CharacterGrid { + scale: f32, + invert: bool, + text_style: Option, +} + +impl CharacterGrid { + pub fn new() -> Self { + Self { + scale: 1.0, + invert: false, + text_style: None, + } + } + + pub fn scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } +} + +impl RenderOnce for CharacterGrid { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let mut theme = SpecimenTheme::default(); + + if self.invert { + theme = theme.invert(); + } + + let characters = vec![ + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G", + "H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", + "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q", + "r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β", + "Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ", + "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р", + "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*", + "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ", + "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎", + ]; + + let columns = 20; + let rows = characters.len().div_ceil(columns); + + let grid_rows = (0..rows).map(|row_idx| { + let start_idx = row_idx * columns; + let end_idx = (start_idx + columns).min(characters.len()); + + div() + .w_full() + .flex() + .flex_row() + .children((start_idx..end_idx).map(|i| { + div() + .text_center() + .size(px(62.)) + .bg(theme.bg) + .text_color(theme.fg) + .text_size(px(24.0)) + .line_height(relative(1.0)) + .child(characters[i]) + })) + .when(end_idx - start_idx < columns, |d| { + d.children( + iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)), + ) + }) + }); + + div().p_4().gap_2().flex().flex_col().children(grid_rows) + } +} + +struct TextExample { + next_id: usize, + font_family: SharedString, +} + +impl TextExample { + fn next_id(&mut self) -> usize { + self.next_id += 1; + self.next_id + } + + fn button( + text: &str, + cx: &mut Context, + on_click: impl Fn(&mut Self, &mut Context) + 'static, + ) -> impl IntoElement { + div() + .id(text.to_string()) + .flex_none() + .child(text.to_string()) + .bg(gpui::black()) + .text_color(gpui::white()) + .active(|this| this.opacity(0.8)) + .px_3() + .py_1() + .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) + } +} + +const FONT_FAMILIES: [&str; 5] = [ + ".ZedMono", + ".SystemUIFont", + "Menlo", + "Monaco", + "Courier New", +]; + +impl Render for TextExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let tcx = cx.text_context(); + let colors = cx.default_colors().clone(); + + let font_size = tcx.font_size; + let line_height = tcx.line_height; + let type_scale = tcx.type_scale; + + let step_down_2 = 1.0 / (type_scale * type_scale); + let step_down_1 = 1.0 / type_scale; + let base = 1.0; + let step_up_1 = base * type_scale; + let step_up_2 = step_up_1 * type_scale; + let step_up_3 = step_up_2 * type_scale; + let step_up_4 = step_up_3 * type_scale; + let step_up_5 = step_up_4 * type_scale; + let step_up_6 = step_up_5 * type_scale; + + div() + .font_family(self.font_family.clone()) + .size_full() + .child( + div() + .bg(gpui::white()) + .border_b_1() + .border_color(gpui::black()) + .p_3() + .flex() + .child(Self::button(&self.font_family, cx, |this, cx| { + let new_family = FONT_FAMILIES + .iter() + .position(|f| *f == this.font_family.as_str()) + .map(|idx| FONT_FAMILIES[(idx + 1) % FONT_FAMILIES.len()]) + .unwrap_or(FONT_FAMILIES[0]); + + this.font_family = SharedString::new(new_family); + cx.notify(); + })), + ) + .child( + div() + .id("text-example") + .overflow_y_scroll() + .overflow_x_hidden() + .bg(rgb(0xffffff)) + .size_full() + .child( + div() + .flex() + .flex_col() + .p_3() + .text_size(px(font_size)) + .line_height(relative(line_height)) + .children([ + div().child("CJK: 中文「日本語」한글"), + div().child( + "Kana: が / か\u{3099}; Hangul: 각 / \u{1100}\u{1161}\u{11a8}", + ), + div().child("Emoji: 😀 ❤️ ❤\u{fe0e} 👍🏽 🇯🇵 1\u{fe0f}\u{20e3} 👩‍💻 👨‍👩‍👧‍👦"), + ]), + ) + .child(div().child(CharacterGrid::new().scale(base))) + .child( + div() + .child(Specimen::new(self.next_id()).scale(step_down_2)) + .child(Specimen::new(self.next_id()).scale(step_down_2).invert()) + .child(Specimen::new(self.next_id()).scale(step_down_1)) + .child(Specimen::new(self.next_id()).scale(step_down_1).invert()) + .child(Specimen::new(self.next_id()).scale(base)) + .child(Specimen::new(self.next_id()).scale(base).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_1)) + .child(Specimen::new(self.next_id()).scale(step_up_1).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_2)) + .child(Specimen::new(self.next_id()).scale(step_up_2).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_3)) + .child(Specimen::new(self.next_id()).scale(step_up_3).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_4)) + .child(Specimen::new(self.next_id()).scale(step_up_4).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_5)) + .child(Specimen::new(self.next_id()).scale(step_up_5).invert()) + .child(Specimen::new(self.next_id()).scale(step_up_6)) + .child(Specimen::new(self.next_id()).scale(step_up_6).invert()), + ), + ) + .child(div().w(px(240.)).h_full().bg(colors.container)) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + cx.set_menus(vec![Menu { + name: "GPUI Typography".into(), + disabled: false, + items: vec![], + }]); + + let fonts = [include_bytes!( + "../../../assets/fonts/lilex/Lilex-Regular.ttf" + )] + .iter() + .map(|b| Cow::Borrowed(&b[..])) + .collect(); + + _ = cx.text_system().add_fonts(fonts); + + cx.init_colors(); + cx.set_global(GlobalTextContext(Arc::new(TextContext::default()))); + + let window = cx + .open_window( + WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some("GPUI Typography".into()), + ..Default::default() + }), + window_bounds: Some(WindowBounds::Windowed(bounds( + point(px(0.0), px(0.0)), + size(px(920.), px(720.)), + ))), + ..Default::default() + }, + |_window, cx| { + cx.new(|_cx| TextExample { + next_id: 0, + font_family: ".ZedMono".into(), + }) + }, + ) + .unwrap(); + + window + .update(cx, |_view, _window, cx| { + cx.activate(true); + }) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/text_layout.rs b/vendor/gpui-pre/examples/text_layout.rs new file mode 100644 index 0000000..07f560a --- /dev/null +++ b/vendor/gpui-pre/examples/text_layout.rs @@ -0,0 +1,117 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, WindowOptions, + div, prelude::*, px, size, +}; +use gpui_platform::application; + +struct HelloWorld {} + +impl Render for HelloWorld { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .bg(gpui::white()) + .flex() + .flex_col() + .gap_2() + .p_4() + .size_full() + .child(div().child("Text left")) + .child(div().text_center().child("Text center")) + .child(div().text_right().child("Text right")) + .child(div().text_decoration_1().child("Text left (underline)")) + .child( + div() + .text_center() + .text_decoration_1() + .child("Text center (underline)"), + ) + .child( + div() + .text_right() + .text_decoration_1() + .child("Text right (underline)"), + ) + .child(div().line_through().child("Text left (line_through)")) + .child( + div() + .text_center() + .line_through() + .child("Text center (line_through)"), + ) + .child( + div() + .text_right() + .line_through() + .child("Text right (line_through)"), + ) + .child( + div() + .flex() + .gap_2() + .justify_between() + .child( + div() + .w(px(400.)) + .border_1() + .border_color(gpui::blue()) + .p_1() + .whitespace_nowrap() + .overflow_hidden() + .text_center() + .child("A long non-wrapping text align center"), + ) + .child( + div() + .w_32() + .border_1() + .border_color(gpui::blue()) + .p_1() + .whitespace_nowrap() + .overflow_hidden() + .text_right() + .child("100%"), + ), + ) + .child(div().flex().gap_2().justify_between().child( + StyledText::new("ABCD").with_highlights([ + (0..1, FontWeight::EXTRA_BOLD.into()), + (2..3, FontStyle::Italic.into()), + ]), + )) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| HelloWorld {}), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/text_wrapper.rs b/vendor/gpui-pre/examples/text_wrapper.rs new file mode 100644 index 0000000..51e58fc --- /dev/null +++ b/vendor/gpui-pre/examples/text_wrapper.rs @@ -0,0 +1,144 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px, + size, +}; +use gpui_platform::application; + +struct HelloWorld {} + +impl Render for HelloWorld { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \ + English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \ + refers to a lung disease contracted from the inhalation of very fine silica particles, \ + a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \ + specifically from a volcano; medically, it is the same as silicosis."; + div() + .id("page") + .size_full() + .flex() + .flex_col() + .p_2() + .gap_2() + .bg(gpui::white()) + .child( + div() + .flex() + .flex_row() + .flex_shrink_0() + .gap_2() + .child( + div() + .flex() + .border_1() + .border_color(gpui::red()) + .text_ellipsis() + .child("longer text in flex 1"), + ) + .child( + div() + .flex() + .border_1() + .border_color(gpui::red()) + .text_ellipsis() + .child("short flex"), + ) + .child( + div() + .overflow_hidden() + .border_1() + .border_color(gpui::red()) + .text_ellipsis() + .w_full() + .child("A short text in normal div"), + ), + ) + .child( + div() + .flex_shrink_0() + .text_xl() + .truncate() + .border_1() + .border_color(gpui::blue()) + .child("ELLIPSIS: ".to_owned() + text), + ) + .child( + div() + .flex_shrink_0() + .text_xl() + .overflow_hidden() + .text_ellipsis() + .line_clamp(2) + .border_1() + .border_color(gpui::blue()) + .child("ELLIPSIS 2 lines: ".to_owned() + text), + ) + .child( + div() + .flex_shrink_0() + .text_xl() + .overflow_hidden() + .text_overflow(TextOverflow::Truncate("".into())) + .border_1() + .border_color(gpui::green()) + .child("TRUNCATE: ".to_owned() + text), + ) + .child( + div() + .flex_shrink_0() + .text_xl() + .overflow_hidden() + .text_overflow(TextOverflow::Truncate("".into())) + .line_clamp(3) + .border_1() + .border_color(gpui::green()) + .child("TRUNCATE 3 lines: ".to_owned() + text), + ) + .child( + div() + .flex_shrink_0() + .text_xl() + .whitespace_nowrap() + .overflow_hidden() + .border_1() + .border_color(gpui::black()) + .child("NOWRAP: ".to_owned() + text), + ) + .child(div().text_xl().w_full().child(text)) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| HelloWorld {}), + ) + .unwrap(); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/toggle_headless.rs b/vendor/gpui-pre/examples/toggle_headless.rs new file mode 100644 index 0000000..136e31f --- /dev/null +++ b/vendor/gpui-pre/examples/toggle_headless.rs @@ -0,0 +1,309 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[cfg(not(target_family = "wasm"))] +use std::{ + io::{self, BufRead as _}, + sync::mpsc, + time::Duration, +}; + +#[cfg(not(target_family = "wasm"))] +use gpui::{ + AnyWindowHandle, App, AppContext as _, AsyncApp, Context, Entity, GraphicalEnvironment, + QuitMode, Render, Subscription, TitlebarOptions, Window, WindowOptions, WindowingRequest, div, + prelude::*, +}; + +#[cfg(not(target_family = "wasm"))] +struct Todo { + message: String, +} + +#[cfg(not(target_family = "wasm"))] +struct Todos { + items: Vec>, + window: Option, +} + +#[cfg(not(target_family = "wasm"))] +struct TodoWindow { + todos: Entity, + _todos_subscription: Subscription, +} + +#[cfg(not(target_family = "wasm"))] +impl Render for TodoWindow { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let rows = self + .todos + .read(cx) + .items + .iter() + .enumerate() + .map(|(index, todo)| div().child(describe_todo(index, todo, cx))) + .collect::>(); + + div() + .size_full() + .bg(gpui::rgb(0xffffff)) + .text_color(gpui::rgb(0x202020)) + .flex() + .flex_col() + .gap_2() + .p_4() + .child("Switchable display todos") + .child("Use the terminal: create [message], ls, close, open, quit") + .when(rows.is_empty(), |view| { + view.child("No todos yet. Type `create` in the terminal.") + }) + .children(rows) + } +} + +/// English, Chinese, Japanese and Korean for a word, then an emoji for it. +#[cfg(not(target_family = "wasm"))] +const SAMPLE_TODOS: [&str; 5] = [ + "milk / 牛奶 / 牛乳 / 우유 🥛", + "apple / 苹果 / りんご / 사과 🍎", + "cat / 猫 / ねこ / 고양이 🐱", + "book / 书 / 本 / 책 📚", + "rain / 雨 / あめ / 비 🌧️", +]; + +#[cfg(not(target_family = "wasm"))] +fn describe_todo(index: usize, todo: &Entity, cx: &App) -> String { + let todo_reference: &Todo = todo.read(cx); + format!( + "{index} - {} - {:?} @{:p}", + todo_reference.message, + todo.entity_id(), + todo_reference + ) +} + +#[cfg(not(target_family = "wasm"))] +fn print_todos(todos: &Entity, cx: &App) { + if todos.read(cx).items.is_empty() { + println!("no todos"); + } + for (index, todo) in todos.read(cx).items.iter().enumerate() { + println!("{}", describe_todo(index, todo, cx)); + } +} + +#[cfg(not(target_family = "wasm"))] +fn open_window(todos: &Entity, cx: &mut App) -> anyhow::Result<()> { + if todos.read(cx).window.is_some() { + return Ok(()); + } + + let options = WindowOptions { + titlebar: Some(TitlebarOptions { + title: Some("Switchable display todos".into()), + ..Default::default() + }), + ..Default::default() + }; + let handle = cx.open_window(options, |_, cx| { + cx.new(|cx| TodoWindow { + todos: todos.clone(), + _todos_subscription: cx.observe(todos, |_, _, cx| cx.notify()), + }) + })?; + // On macOS, making the window key does not activate the app over the terminal. + cx.activate(true); + handle.update(cx, |_, window, _| window.activate_window())?; + todos.update(cx, |todos, _| todos.window = Some(handle.into())); + Ok(()) +} + +/// Builds the environment for `open`: the given `NAME=value` pairs, or this process's +/// environment when none are given. +#[cfg(target_os = "linux")] +fn display_environment(arguments: &str) -> anyhow::Result { + if arguments.trim().is_empty() { + return Ok(GraphicalEnvironment::detect()); + } + let mut environment = GraphicalEnvironment::default(); + for assignment in arguments.split_whitespace() { + let (name, value) = assignment + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("expected NAME=value, got {assignment:?}"))?; + match name { + "WAYLAND_DISPLAY" => environment.wayland_display = Some(value.into()), + "DISPLAY" => environment.x11_display = Some(value.into()), + "XDG_RUNTIME_DIR" => environment.xdg_runtime_dir = Some(value.into()), + "XDG_ACTIVATION_TOKEN" => environment.activation_token = Some(value.into()), + _ => anyhow::bail!("unknown display variable {name}"), + } + } + Ok(environment) +} + +/// Builds the environment for `open`: `SESSION=id`, or this process's session when not given. +#[cfg(target_os = "windows")] +fn display_environment(arguments: &str) -> anyhow::Result { + let mut environment = GraphicalEnvironment::detect(); + for assignment in arguments.split_whitespace() { + let session = assignment + .strip_prefix("SESSION=") + .ok_or_else(|| anyhow::anyhow!("expected SESSION=id, got {assignment:?}"))?; + environment.session_id = Some(session.parse()?); + } + Ok(environment) +} + +/// Builds the environment for `open`, which carries nothing on this platform. +#[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))] +fn display_environment(_arguments: &str) -> anyhow::Result { + Ok(GraphicalEnvironment::detect()) +} + +#[cfg(not(target_family = "wasm"))] +fn handle_command( + command: &str, + todos: &Entity, + cx: &mut App, +) -> anyhow::Result> { + let (name, arguments) = command.split_once(' ').unwrap_or((command, "")); + match name { + "ls" => print_todos(todos, cx), + "create" => { + // A sample with CJK text and an emoji, so that drawing todos exercises color glyph + // rasterization, which uses the GPU devices a switch to windowed mode creates. + let count = todos.read(cx).items.len(); + let sample = SAMPLE_TODOS[count % SAMPLE_TODOS.len()]; + let message = if arguments.trim().is_empty() { + sample.to_owned() + } else { + format!("{arguments} · {sample}") + }; + let todo = cx.new(|_| Todo { message }); + todos.update(cx, |todos, cx| { + todos.items.push(todo); + println!("todo added, total {}", todos.items.len()); + cx.notify(); + }); + } + "open" => { + return Ok(Some(WindowingRequest::Windowed(display_environment( + arguments, + )?))); + } + "close" => { + let window = todos.update(cx, |todos, _| todos.window.take()); + if let Some(window) = window { + window.update(cx, |_, window, _| window.remove_window())?; + } + return Ok(Some(WindowingRequest::Headless)); + } + "quit" => cx.quit(), + "" => {} + _ => println!("{USAGE}"), + } + Ok(None) +} + +#[cfg(target_os = "linux")] +const USAGE: &str = "commands: ls | create [message] | open [DISPLAY=… WAYLAND_DISPLAY=… XDG_RUNTIME_DIR=… XDG_ACTIVATION_TOKEN=…] | close | quit"; + +#[cfg(target_os = "windows")] +const USAGE: &str = "commands: ls | create [message] | open [SESSION=…] | close | quit"; + +#[cfg(not(any(target_os = "linux", target_os = "windows", target_family = "wasm")))] +const USAGE: &str = "commands: ls | create [message] | open | close | quit"; + +#[cfg(not(target_family = "wasm"))] +async fn run_command( + command: String, + todos: &Entity, + cx: &mut AsyncApp, +) -> anyhow::Result<()> { + match cx.update(|cx| handle_command(&command, todos, cx))? { + None => return Ok(()), + Some(request @ WindowingRequest::Headless) => { + if cx.update(|cx| cx.graphical_environment()).is_some() { + cx.update(|cx| cx.request_windowing(request)).await?; + } + } + Some(request @ WindowingRequest::Windowed(_)) => { + // Switching fails while windowed, so `open` then just opens the window. + if cx.update(|cx| cx.graphical_environment()).is_none() { + cx.update(|cx| cx.request_windowing(request)).await?; + } + cx.update(|cx| open_window(todos, cx))?; + } + } + let mode = cx.update(|cx| match cx.graphical_environment() { + None => "headless".to_owned(), + Some(_) => match cx.compositor_name() { + "" => "windowed".to_owned(), + compositor => compositor.to_owned(), + }, + }); + println!("mode: {mode}"); + Ok(()) +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + #[cfg(target_os = "linux")] + let application = gpui_platform::linux(gpui::WindowingModes::all()); + #[cfg(not(target_os = "linux"))] + let application = gpui_platform::application(); + application + .with_windowing(WindowingRequest::Headless) + .with_quit_mode(QuitMode::Explicit) + .run(|cx| { + let todos = cx.new(|_| Todos { + items: Vec::new(), + window: None, + }); + let window_closed_subscription = cx.on_window_closed({ + let todos = todos.clone(); + move |cx, window_id| { + todos.update(cx, |todos, _| { + if todos + .window + .as_ref() + .is_some_and(|window| window.window_id() == window_id) + { + todos.window = None; + } + }); + } + }); + let (command_sender, command_receiver) = mpsc::channel(); + std::thread::spawn(move || { + for line in io::stdin().lock().lines() { + match line { + Ok(line) => { + if command_sender.send(line).is_err() { + break; + } + } + Err(error) => { + eprintln!("stdin error: {error}"); + break; + } + } + } + }); + + println!("{USAGE}"); + cx.spawn(async move |cx| { + let _window_closed_subscription = window_closed_subscription; + loop { + cx.background_executor() + .timer(Duration::from_millis(25)) + .await; + while let Ok(command) = command_receiver.try_recv() { + if let Err(error) = run_command(command, &todos, cx).await { + println!("error: {error:#}"); + } + } + } + }) + .detach(); + }); +} diff --git a/vendor/gpui-pre/examples/tree.rs b/vendor/gpui-pre/examples/tree.rs new file mode 100644 index 0000000..891be47 --- /dev/null +++ b/vendor/gpui-pre/examples/tree.rs @@ -0,0 +1,64 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can +//! handle deep hierarchies (even though it cannot just yet!). +#[path = "example_support/fonts.rs"] +mod example_support; + +use std::sync::LazyLock; + +use gpui::{App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, size}; +use gpui_platform::application; + +struct Tree {} + +static DEPTH: LazyLock = LazyLock::new(|| { + std::env::var("GPUI_TREE_DEPTH") + .ok() + .and_then(|depth| depth.parse().ok()) + .unwrap_or_else(|| 50) +}); + +impl Render for Tree { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let mut depth = *DEPTH; + static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()]; + let mut colors = COLORS.iter().cycle().copied(); + let mut next_div = || div().p_0p5().bg(colors.next().unwrap()); + let mut innermost_node = next_div(); + while depth > 0 { + innermost_node = next_div().child(innermost_node); + depth -= 1; + } + innermost_node + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Tree {}), + ) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/uniform_list.rs b/vendor/gpui-pre/examples/uniform_list.rs new file mode 100644 index 0000000..4a2c478 --- /dev/null +++ b/vendor/gpui-pre/examples/uniform_list.rs @@ -0,0 +1,71 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size, + uniform_list, +}; +use gpui_platform::application; + +struct UniformListExample {} + +impl Render for UniformListExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div().size_full().bg(rgb(0xffffff)).child( + uniform_list( + "entries", + 50, + cx.processor(|_this, range, _window, _cx| { + let mut items = Vec::new(); + for ix in range { + let item = ix + 1; + + items.push( + div() + .id(ix) + .px_2() + .cursor_pointer() + .on_click(move |_event, _window, _cx| { + println!("clicked Item {item:?}"); + }) + .child(format!("Item {item}")), + ); + } + items + }), + ) + .h_full(), + ) + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| UniformListExample {}), + ) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/view_example/example_editor.rs b/vendor/gpui-pre/examples/view_example/example_editor.rs new file mode 100644 index 0000000..2064d7c --- /dev/null +++ b/vendor/gpui-pre/examples/view_example/example_editor.rs @@ -0,0 +1,549 @@ +//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard +//! handling, and the specialized text-shaping renderer. The *text itself* lives +//! in a shared `Entity` it's handed at construction, so the value is +//! readable/writable from outside while the editing machinery stays in here. +//! +//! This is the piece that proves the point: a text input is genuinely +//! complicated, and `View` lets all of that complexity live in one entity that +//! anything can embed. + +use std::ops::Range; +use std::time::Duration; + +use gpui::{ + App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable, + InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task, + TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size, +}; +use unicode_segmentation::*; + +use crate::{Backspace, Delete, End, Home, Left, Right}; + +pub struct Editor { + pub value: Entity, + pub focus_handle: FocusHandle, + pub cursor: usize, + pub cursor_visible: bool, + _blink_task: Task<()>, + _subscriptions: Vec, +} + +impl Editor { + /// An editor that owns its own string internally, seeded with `text`. + /// Nothing to allocate or wire up at the call site. + pub fn new(text: impl Into, window: &mut Window, cx: &mut Context) -> Self { + let value = cx.new(|_| text.into()); + Self::over(value, window, cx) + } + + /// An editor over a string *you* own, so the value is shared in and out. + pub fn over(value: Entity, window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + + let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| { + this.start_blink(cx); + }); + let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| { + this.stop_blink(cx); + }); + + // The value is shared: anything can write it while we hold a cursor into + // it. Observe it so external writes (a) clamp the cursor back onto a char + // boundary before the next IME round-trip can slice out of bounds, and + // (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache + // is keyed on *our* notify, not the value's. + let value_sub = cx.observe(&value, |this, value, cx| { + let content = value.read(cx); + let mut cursor = this.cursor.min(content.len()); + while cursor > 0 && !content.is_char_boundary(cursor) { + cursor -= 1; + } + this.cursor = cursor; + cx.notify(); + }); + + Self { + value, + focus_handle, + cursor: 0, + cursor_visible: false, + _blink_task: Task::ready(()), + _subscriptions: vec![focus_sub, blur_sub, value_sub], + } + } + + /// The current text. Read this from anywhere to get the value out. + pub fn text(&self, cx: &App) -> String { + self.value.read(cx).clone() + } + + fn start_blink(&mut self, cx: &mut Context) { + self.cursor_visible = true; + self._blink_task = Self::spawn_blink_task(cx); + } + + fn stop_blink(&mut self, cx: &mut Context) { + self.cursor_visible = false; + self._blink_task = Task::ready(()); + cx.notify(); + } + + fn spawn_blink_task(cx: &mut Context) -> Task<()> { + cx.spawn(async move |this, cx| { + loop { + cx.background_executor() + .timer(Duration::from_millis(500)) + .await; + let result = this.update(cx, |editor, cx| { + editor.cursor_visible = !editor.cursor_visible; + cx.notify(); + }); + if result.is_err() { + break; + } + } + }) + } + + fn reset_blink(&mut self, cx: &mut Context) { + self.cursor_visible = true; + self._blink_task = Self::spawn_blink_task(cx); + } + + pub fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context) { + let content = self.text(cx); + if self.cursor > 0 { + self.cursor = previous_boundary(&content, self.cursor); + } + self.reset_blink(cx); + cx.notify(); + } + + pub fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context) { + let content = self.text(cx); + if self.cursor < content.len() { + self.cursor = next_boundary(&content, self.cursor); + } + self.reset_blink(cx); + cx.notify(); + } + + pub fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context) { + self.cursor = 0; + self.reset_blink(cx); + cx.notify(); + } + + pub fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context) { + self.cursor = self.text(cx).len(); + self.reset_blink(cx); + cx.notify(); + } + + pub fn backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context) { + let content = self.text(cx); + if self.cursor > 0 { + let prev = previous_boundary(&content, self.cursor); + let cursor = self.cursor; + self.value.update(cx, |s, cx| { + s.drain(prev..cursor); + cx.notify(); + }); + self.cursor = prev; + } + self.reset_blink(cx); + cx.notify(); + } + + pub fn delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context) { + let content = self.text(cx); + if self.cursor < content.len() { + let next = next_boundary(&content, self.cursor); + let cursor = self.cursor; + self.value.update(cx, |s, cx| { + s.drain(cursor..next); + cx.notify(); + }); + } + self.reset_blink(cx); + cx.notify(); + } + + pub fn insert_newline(&mut self, cx: &mut Context) { + let cursor = self.cursor; + self.value.update(cx, |s, cx| { + s.insert(cursor, '\n'); + cx.notify(); + }); + self.cursor += 1; + self.reset_blink(cx); + cx.notify(); + } +} + +fn previous_boundary(content: &str, offset: usize) -> usize { + content + .grapheme_indices(true) + .rev() + .find_map(|(idx, _)| (idx < offset).then_some(idx)) + .unwrap_or(0) +} + +fn next_boundary(content: &str, offset: usize) -> usize { + content + .grapheme_indices(true) + .find_map(|(idx, _)| (idx > offset).then_some(idx)) + .unwrap_or(content.len()) +} + +fn offset_from_utf16(content: &str, offset: usize) -> usize { + let mut utf8_offset = 0; + let mut utf16_count = 0; + for ch in content.chars() { + if utf16_count >= offset { + break; + } + utf16_count += ch.len_utf16(); + utf8_offset += ch.len_utf8(); + } + utf8_offset +} + +fn offset_to_utf16(content: &str, offset: usize) -> usize { + let mut utf16_offset = 0; + let mut utf8_count = 0; + for ch in content.chars() { + if utf8_count >= offset { + break; + } + utf8_count += ch.len_utf8(); + utf16_offset += ch.len_utf16(); + } + utf16_offset +} + +fn range_to_utf16(content: &str, range: &Range) -> Range { + offset_to_utf16(content, range.start)..offset_to_utf16(content, range.end) +} + +fn range_from_utf16(content: &str, range_utf16: &Range) -> Range { + offset_from_utf16(content, range_utf16.start)..offset_from_utf16(content, range_utf16.end) +} + +impl Focusable for Editor { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl EntityInputHandler for Editor { + fn text_for_range( + &mut self, + range_utf16: Range, + actual_range: &mut Option>, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + let content = self.text(cx); + let range = range_from_utf16(&content, &range_utf16); + actual_range.replace(range_to_utf16(&content, &range)); + Some(content[range].to_string()) + } + + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + cx: &mut Context, + ) -> Option { + let content = self.text(cx); + let utf16_cursor = offset_to_utf16(&content, self.cursor); + Some(UTF16Selection { + range: utf16_cursor..utf16_cursor, + reversed: false, + }) + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + _window: &mut Window, + cx: &mut Context, + ) { + let content = self.text(cx); + let range = range_utf16 + .as_ref() + .map(|r| range_from_utf16(&content, r)) + .unwrap_or(self.cursor..self.cursor); + + let new_content = content[..range.start].to_owned() + new_text + &content[range.end..]; + self.cursor = range.start + new_text.len(); + self.value.update(cx, |s, cx| { + *s = new_content; + cx.notify(); + }); + self.reset_blink(cx); + cx.notify(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + _new_selected_range_utf16: Option>, + window: &mut Window, + cx: &mut Context, + ) { + self.replace_text_in_range(range_utf16, new_text, window, cx); + } + + fn bounds_for_range( + &mut self, + _range_utf16: Range, + _bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _point: gpui::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } +} + +impl gpui::Render for Editor { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + EditorText { + editor: cx.entity(), + } + } +} + +// --------------------------------------------------------------------------- +// EditorText — the specialized renderer: shapes the text and paints the cursor. +// --------------------------------------------------------------------------- + +struct EditorText { + editor: Entity, +} + +struct EditorTextPrepaint { + lines: Vec, + cursor: Option, +} + +impl IntoElement for EditorText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for EditorText { + type RequestLayoutState = (); + type PrepaintState = EditorTextPrepaint; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let editor = self.editor.read(cx); + let content = editor.value.read(cx); + let line_count = content.split('\n').count().max(1); + let line_height = window.line_height(); + let mut style = gpui::Style::default(); + style.size.width = relative(1.).into(); + style.size.height = (line_height * line_count as f32).into(); + (window.request_layout(style, [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + let editor = self.editor.read(cx); + let content = editor.value.read(cx).clone(); + let cursor_offset = editor.cursor; + let cursor_visible = editor.cursor_visible; + let is_focused = editor.focus_handle.is_focused(window); + + let style = window.text_style(); + let text_color = style.color; + let font_size = style.font_size.to_pixels(window.rem_size()); + let line_height = window.line_height(); + + let is_placeholder = content.is_empty(); + + let lines: Vec = if is_placeholder { + let placeholder: SharedString = "Type here...".into(); + let run = TextRun { + len: placeholder.len(), + font: style.font(), + color: hsla(0., 0., 0.5, 0.5), + background_color: None, + underline: None, + strikethrough: None, + }; + vec![ + window + .text_system() + .shape_line(placeholder, font_size, &[run], None), + ] + } else { + content + .split('\n') + .map(|line_str| { + let text: SharedString = SharedString::from(line_str.to_string()); + let run = TextRun { + len: text.len(), + font: style.font(), + color: text_color, + background_color: None, + underline: None, + strikethrough: None, + }; + window + .text_system() + .shape_line(text, font_size, &[run], None) + }) + .collect() + }; + + let cursor = if is_focused && cursor_visible && !is_placeholder { + let (cursor_line, offset_in_line) = cursor_line_and_offset(&content, cursor_offset); + let cursor_line = cursor_line.min(lines.len().saturating_sub(1)); + let cursor_x = lines[cursor_line].x_for_index(offset_in_line); + Some(fill( + Bounds::new( + point( + bounds.left() + cursor_x, + bounds.top() + line_height * cursor_line as f32, + ), + size(px(1.5), line_height), + ), + text_color, + )) + } else if is_focused && cursor_visible && is_placeholder { + Some(fill( + Bounds::new( + point(bounds.left(), bounds.top()), + size(px(1.5), line_height), + ), + text_color, + )) + } else { + None + }; + + EditorTextPrepaint { lines, cursor } + } + + fn paint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let focus_handle = self.editor.read(cx).focus_handle.clone(); + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + cx, + ); + + let line_height = window.line_height(); + for (i, line) in prepaint.lines.iter().enumerate() { + let origin = point(bounds.left(), bounds.top() + line_height * i as f32); + line.paint(origin, line_height, gpui::TextAlign::Left, None, window, cx) + .unwrap(); + } + + if let Some(cursor) = prepaint.cursor.take() { + window.paint_quad(cursor); + } + } +} + +fn cursor_line_and_offset(content: &str, cursor: usize) -> (usize, usize) { + let mut line_index = 0; + let mut line_start = 0; + for (i, ch) in content.char_indices() { + if i >= cursor { + break; + } + if ch == '\n' { + line_index += 1; + line_start = i + 1; + } + } + (line_index, cursor - line_start) +} + +pub fn standard_actions(editor: Entity) -> impl FnOnce(E) -> E { + move |element| { + element + .on_action({ + let editor = editor.clone(); + move |a: &Left, window, cx| editor.update(cx, |e, cx| e.left(a, window, cx)) + }) + .on_action({ + let editor = editor.clone(); + move |a: &Right, window, cx| editor.update(cx, |e, cx| e.right(a, window, cx)) + }) + .on_action({ + let editor = editor.clone(); + move |a: &Home, window, cx| editor.update(cx, |e, cx| e.home(a, window, cx)) + }) + .on_action({ + let editor = editor.clone(); + move |a: &End, window, cx| editor.update(cx, |e, cx| e.end(a, window, cx)) + }) + .on_action({ + let editor = editor.clone(); + move |a: &Backspace, window, cx| { + editor.update(cx, |e, cx| e.backspace(a, window, cx)) + } + }) + .on_action(move |a: &Delete, window, cx| { + editor.update(cx, |e, cx| e.delete(a, window, cx)) + }) + } +} diff --git a/vendor/gpui-pre/examples/view_example/example_input.rs b/vendor/gpui-pre/examples/view_example/example_input.rs new file mode 100644 index 0000000..25d7401 --- /dev/null +++ b/vendor/gpui-pre/examples/view_example/example_input.rs @@ -0,0 +1,121 @@ +//! `Input` — a single-line text input. The shaping layer over `Editor`. +//! +//! Construct it two ways, depending on how much state you want to own: +//! * `Input::new(value: Entity)` — you hold just the string; the input +//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden. +//! * `Input::editor(editor: Entity)` — you hold the editor; cursor/selection +//! are now yours to read and drive too. +//! +//! Either way the chrome is identical. Because the string (or editor) is the +//! input's *identity*, the internal `use_state(Editor)` is collision-safe across +//! any number of inputs. + +use gpui::{ + App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement, + Window, div, hsla, point, prelude::*, px, white, +}; + +use crate::example_editor::{Editor, standard_actions}; + +enum Source { + Value(Entity), + Editor(Entity), +} + +#[derive(IntoElement)] +pub struct Input { + source: Source, + width: Option, + color: Option, +} + +impl Input { + /// Backed by a bare string; the editor is allocated internally. + pub fn new(value: Entity) -> Self { + Self { + source: Source::Value(value), + width: None, + color: None, + } + } + + /// Backed by an editor you own (so you can read/drive its cursor). + pub fn editor(editor: Entity) -> Self { + Self { + source: Source::Editor(editor), + width: None, + color: None, + } + } + + pub fn width(mut self, width: Pixels) -> Self { + self.width = Some(width); + self + } + + pub fn color(mut self, color: Hsla) -> Self { + self.color = Some(color); + self + } +} + +impl gpui::View for Input { + fn entity_id(&self) -> Option { + Some(match &self.source { + Source::Value(value) => value.entity_id(), + Source::Editor(editor) => editor.entity_id(), + }) + } + + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + // Get the editor: use the one we were handed, or allocate it under our + // own (string-derived) identity so it persists and never collides. + let editor = match self.source { + Source::Value(value) => { + window.use_state(cx, move |window, cx| Editor::over(value, window, cx)) + } + Source::Editor(editor) => editor, + }; + + let focus_handle = editor.read(cx).focus_handle.clone(); + let is_focused = focus_handle.is_focused(window); + let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.)); + let box_width = self.width.unwrap_or(px(300.)); + + let border = if is_focused { + hsla(220. / 360., 0.8, 0.5, 1.) + } else { + hsla(0., 0., 0.75, 1.) + }; + + div() + .id("input") + .key_context("TextInput") + .track_focus(&focus_handle) + .cursor(CursorStyle::IBeam) + .map(standard_actions(editor.clone())) + .w(box_width) + .h(px(36.)) + .px(px(8.)) + .bg(white()) + .border_1() + .border_color(border) + .when(is_focused, |this| { + this.shadow(vec![BoxShadow { + color: hsla(220. / 360., 0.8, 0.5, 0.3), + offset: point(px(0.), px(0.)), + blur_radius: px(4.), + spread_radius: px(1.), + inset: false, + }]) + }) + .rounded(px(4.)) + .overflow_hidden() + .flex() + .items_center() + .line_height(px(20.)) + .text_size(px(14.)) + .text_color(text_color) + .child(editor.cached(StyleRefinement::default().size_full())) + } +} diff --git a/vendor/gpui-pre/examples/view_example/example_tests.rs b/vendor/gpui-pre/examples/view_example/example_tests.rs new file mode 100644 index 0000000..a3edae8 --- /dev/null +++ b/vendor/gpui-pre/examples/view_example/example_tests.rs @@ -0,0 +1,131 @@ +//! Tests for the input composition. Require the `test-support` feature: +//! +//! ```sh +//! cargo test -p gpui --example view_example --features test-support +//! ``` + +#[cfg(test)] +mod tests { + use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*}; + + use crate::example_editor::Editor; + use crate::example_input::Input; + use crate::{Backspace, Delete, End, Home, Left, Right}; + + /// Two inputs, each backed by an editor we own (so the test can focus and + /// read them). Proves data flows through the shared `String` and that + /// sibling inputs stay isolated. + struct Harness { + a: Entity, + b: Entity, + } + + impl Render for Harness { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + gpui::div() + .child(Input::editor(self.a.clone())) + .child(Input::editor(self.b.clone())) + } + } + + fn bind_keys(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.bind_keys([ + KeyBinding::new("backspace", Backspace, None), + KeyBinding::new("delete", Delete, None), + KeyBinding::new("left", Left, None), + KeyBinding::new("right", Right, None), + KeyBinding::new("home", Home, None), + KeyBinding::new("end", End, None), + ]); + }); + } + + fn setup( + cx: &mut TestAppContext, + ) -> ( + Entity, + Entity, + Entity, + &mut gpui::VisualTestContext, + ) { + bind_keys(cx); + + let (harness, cx) = cx.add_window_view(|window, cx| { + let a_value = cx.new(|_| String::new()); + let b_value = cx.new(|_| String::new()); + let a = cx.new(|cx| Editor::over(a_value, window, cx)); + let b = cx.new(|cx| Editor::over(b_value, window, cx)); + Harness { a, b } + }); + + let a = cx.read_entity(&harness, |h, _| h.a.clone()); + let b = cx.read_entity(&harness, |h, _| h.b.clone()); + let a_value = cx.read_entity(&a, |e, _| e.value.clone()); + let b_value = cx.read_entity(&b, |e, _| e.value.clone()); + + // Focus the first input's editor. + cx.update(|window, cx| { + let focus_handle = a.read(cx).focus_handle.clone(); + window.focus(&focus_handle, cx); + }); + + (a, a_value, b_value, cx) + } + + #[gpui::test] + fn typing_updates_the_shared_string(cx: &mut TestAppContext) { + let (editor, a_value, _b_value, cx) = setup(cx); + + cx.simulate_input("hello"); + + cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello")); + cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5)); + } + + #[gpui::test] + fn sibling_inputs_are_isolated(cx: &mut TestAppContext) { + let (_editor, a_value, b_value, cx) = setup(cx); + + cx.simulate_input("x"); + + cx.read_entity(&a_value, |value, _| assert_eq!(value, "x")); + cx.read_entity(&b_value, |value, _| { + assert_eq!(value, "", "typing in input A must not touch input B") + }); + } + + #[gpui::test] + fn external_writes_clamp_the_cursor(cx: &mut TestAppContext) { + let (editor, a_value, _b_value, cx) = setup(cx); + + cx.simulate_input("hello"); + cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5)); + + // Write the shared value from outside the editor. The old cursor (5) + // now points into the middle of a multi-byte character; the editor's + // observation must clamp it back onto a boundary. + cx.update(|_, cx| { + a_value.update(cx, |value, cx| { + *value = "日本".to_string(); + cx.notify(); + }) + }); + + cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本")); + cx.read_entity(&editor, |editor, _| { + assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary"); + }); + } + + #[gpui::test] + fn arrows_move_the_cursor(cx: &mut TestAppContext) { + let (editor, _a_value, _b_value, cx) = setup(cx); + + cx.simulate_input("abc"); + cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 3)); + + cx.simulate_keystrokes("left left"); + cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1)); + } +} diff --git a/vendor/gpui-pre/examples/view_example/example_text_area.rs b/vendor/gpui-pre/examples/view_example/example_text_area.rs new file mode 100644 index 0000000..07640b9 --- /dev/null +++ b/vendor/gpui-pre/examples/view_example/example_text_area.rs @@ -0,0 +1,118 @@ +//! `TextArea` — a multi-line text box. Same `Editor` workhorse, taller chrome, +//! and `Enter` inserts a newline instead of being ignored. Constructible from a +//! string or an editor, exactly like [`Input`](crate::example_input::Input). + +use gpui::{ + App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, StyleRefinement, Window, div, + hsla, point, prelude::*, px, white, +}; + +use crate::Enter; +use crate::example_editor::{Editor, standard_actions}; + +enum Source { + Value(Entity), + Editor(Entity), +} + +#[derive(IntoElement)] +pub struct TextArea { + source: Source, + rows: usize, + color: Option, +} + +impl TextArea { + pub fn new(value: Entity, rows: usize) -> Self { + Self { + source: Source::Value(value), + rows, + color: None, + } + } + + pub fn editor(editor: Entity, rows: usize) -> Self { + Self { + source: Source::Editor(editor), + rows, + color: None, + } + } + + pub fn color(mut self, color: Hsla) -> Self { + self.color = Some(color); + self + } +} + +impl gpui::View for TextArea { + fn entity_id(&self) -> Option { + Some(match &self.source { + Source::Value(value) => value.entity_id(), + Source::Editor(editor) => editor.entity_id(), + }) + } + + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let editor = match self.source { + Source::Value(value) => { + window.use_state(cx, move |window, cx| Editor::over(value, window, cx)) + } + Source::Editor(editor) => editor, + }; + + let focus_handle = editor.read(cx).focus_handle.clone(); + let is_focused = focus_handle.is_focused(window); + let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.)); + let row_height = px(20.); + let box_height = row_height * self.rows as f32 + px(16.); + + let border = if is_focused { + hsla(220. / 360., 0.8, 0.5, 1.) + } else { + hsla(0., 0., 0.75, 1.) + }; + + div() + .id("text-area") + .key_context("TextInput") + .track_focus(&focus_handle) + .cursor(CursorStyle::IBeam) + .map(standard_actions(editor.clone())) + // Enter is the one binding that differs from a single-line input. + .on_action({ + let editor = editor.clone(); + move |_: &Enter, _window, cx| editor.update(cx, |e, cx| e.insert_newline(cx)) + }) + .w(px(400.)) + .h(box_height) + .p(px(8.)) + .bg(white()) + .border_1() + .border_color(border) + .when(is_focused, |this| { + this.shadow(vec![BoxShadow { + color: hsla(220. / 360., 0.8, 0.5, 0.3), + offset: point(px(0.), px(0.)), + blur_radius: px(4.), + spread_radius: px(1.), + inset: false, + }]) + }) + .rounded(px(4.)) + .overflow_hidden() + .line_height(row_height) + .text_size(px(14.)) + .text_color(text_color) + // The cache style is computed from the `rows` prop: change `rows` and + // the editor's cached bounds change, busting its cache and re-laying + // out the text. (`Input` just uses `size_full()` — nothing to vary.) + .child( + editor.cached( + StyleRefinement::default() + .w_full() + .h(row_height * self.rows as f32), + ), + ) + } +} diff --git a/vendor/gpui-pre/examples/view_example/view_example_main.rs b/vendor/gpui-pre/examples/view_example/view_example_main.rs new file mode 100644 index 0000000..d2e4991 --- /dev/null +++ b/vendor/gpui-pre/examples/view_example/view_example_main.rs @@ -0,0 +1,179 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +//! View example — composing a text input from the `View` primitives. +//! +//! The whole point: a text input is deceptively complicated, and `View` makes it +//! easy to compose one. Three pieces, each shown in its own section: +//! +//! * `Editor` — the workhorse entity: cursor, blink, focus, keyboard, and a +//! specialized text renderer. All the hard parts live here. +//! * `String` — the data plane. `editor.text(cx)` / `value.read(cx)` get it out. +//! * `Input` / `TextArea` — the shaping layer. Each takes a `String` (and grows +//! the editor internally) OR an `Editor` (so you can read the cursor). +//! +//! Run: `cargo run -p gpui --example view_example` + +#[path = "../example_support/fonts.rs"] +mod example_support; + +mod example_editor; +mod example_input; +mod example_text_area; + +#[cfg(test)] +mod example_tests; + +use example_editor::Editor; +use example_input::Input; +use example_text_area::TextArea; + +use gpui::{ + App, Bounds, Context, Div, Entity, IntoElement, KeyBinding, Render, SharedString, Window, + WindowBounds, WindowOptions, actions, div, hsla, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +actions!( + view_example, + [Backspace, Delete, Left, Right, Home, End, Enter, Quit] +); + +/// A tiny stateless view that reads an editor's cursor and is composed *beside* +/// the thing editing it — two views over one entity, zero wiring. +#[derive(IntoElement)] +struct CursorReadout { + editor: Entity, +} + +impl CursorReadout { + fn new(editor: Entity) -> Self { + Self { editor } + } +} + +impl gpui::RenderOnce for CursorReadout { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let cursor = self.editor.read(cx).cursor; + div() + .text_sm() + .text_color(hsla(0., 0., 0.45, 1.)) + .child(SharedString::from(format!("cursor @ {cursor}"))) + } +} + +struct ViewExample; + +impl ViewExample { + fn new() -> Self { + Self + } +} + +impl Render for ViewExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // The data plane: plain strings, allocated at the top by the hook. + let name = window.use_state(cx, |_, _| String::new()); + let email = window.use_state(cx, |_, _| String::from("me@example.com")); + let bio = window.use_state(cx, |_, _| String::new()); + // Editors that own their own string internally — no extra wiring up top. + let notes = window.use_state(cx, |window, cx| Editor::new("multi\nline", window, cx)); + let owned = window.use_state(cx, |window, cx| Editor::new("editable", window, cx)); + + div() + .flex() + .flex_col() + .size_full() + .bg(rgb(0xf0f0f0)) + .p(px(24.)) + .gap(px(24.)) + .child( + section("Inputs — from a String (cursor stays internal)") + .child(Input::new(name).width(px(320.))) + .child( + Input::new(email) + .width(px(320.)) + .color(hsla(0., 0., 0.3, 1.)), + ), + ) + .child( + section("Input — from an Editor (read its cursor beside it)").child( + div() + .flex() + .items_center() + .gap(px(12.)) + .child(Input::editor(owned.clone()).width(px(320.))) + .child(CursorReadout::new(owned)), + ), + ) + .child( + section("Text areas — from a String, or from an Editor") + .child(TextArea::new(bio, 3)) + .child( + div() + .flex() + .items_start() + .gap(px(12.)) + .child(TextArea::editor(notes.clone(), 3).color(hsla( + 250. / 360., + 0.7, + 0.4, + 1., + ))) + .child(CursorReadout::new(notes)), + ), + ) + } +} + +/// A labeled vertical section. +fn section(title: &str) -> Div { + div().flex().flex_col().gap(px(8.)).child( + div() + .text_sm() + .text_color(hsla(0., 0., 0.3, 1.)) + .child(SharedString::from(title.to_string())), + ) +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(560.0), px(480.0)), cx); + cx.bind_keys([ + KeyBinding::new("backspace", Backspace, None), + KeyBinding::new("delete", Delete, None), + KeyBinding::new("left", Left, None), + KeyBinding::new("right", Right, None), + KeyBinding::new("home", Home, None), + KeyBinding::new("end", End, None), + KeyBinding::new("enter", Enter, None), + KeyBinding::new("cmd-q", Quit, None), + ]); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| ViewExample::new()), + ) + .unwrap(); + + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.activate(true); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/window.rs b/vendor/gpui-pre/examples/window.rs new file mode 100644 index 0000000..51dbe72 --- /dev/null +++ b/vendor/gpui-pre/examples/window.rs @@ -0,0 +1,355 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind, + WindowOptions, actions, div, prelude::*, px, rgb, size, +}; +use gpui_platform::application; + +struct SubWindow { + custom_titlebar: bool, + is_dialog: bool, +} + +fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement { + div() + .id(text.to_string()) + .flex_none() + .px_2() + .bg(rgb(0xf7f7f7)) + .active(|this| this.opacity(0.85)) + .border_1() + .border_color(rgb(0xe0e0e0)) + .rounded_sm() + .cursor_pointer() + .child(text.to_string()) + .on_click(move |_, window, cx| on_click(window, cx)) +} + +impl Render for SubWindow { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let window_bounds = + WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx)); + + div() + .flex() + .flex_col() + .bg(rgb(0xffffff)) + .size_full() + .gap_2() + .when(self.custom_titlebar, |cx| { + cx.child( + div() + .flex() + .h(px(32.)) + .px_4() + .bg(gpui::blue()) + .text_color(gpui::white()) + .w_full() + .child( + div() + .flex() + .items_center() + .justify_center() + .size_full() + .child("Custom Titlebar"), + ), + ) + }) + .child( + div() + .p_8() + .flex() + .flex_col() + .gap_2() + .child("SubWindow") + .when(self.is_dialog, |div| { + div.child(button("Open Nested Dialog", move |_, cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + kind: WindowKind::Dialog, + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: true, + }) + }, + ) + .unwrap(); + })) + }) + .child(button("Close", |window, _| { + window.remove_window(); + })), + ) + } +} + +struct WindowDemo {} + +impl Render for WindowDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let window_bounds = + WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx)); + + div() + .p_4() + .flex() + .flex_wrap() + .bg(rgb(0xffffff)) + .size_full() + .justify_center() + .content_center() + .gap_2() + .child(button("Normal", move |_, cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Popup", move |_, cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + kind: WindowKind::PopUp, + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Floating", move |_, cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + kind: WindowKind::Floating, + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Dialog", move |_, cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + kind: WindowKind::Dialog, + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: true, + }) + }, + ) + .unwrap(); + })) + .child(button("Custom Titlebar", move |_, cx| { + cx.open_window( + WindowOptions { + titlebar: None, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: true, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Invisible", move |_, cx| { + cx.open_window( + WindowOptions { + show: false, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Unmovable", move |_, cx| { + cx.open_window( + WindowOptions { + is_movable: false, + titlebar: None, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Unresizable", move |_, cx| { + cx.open_window( + WindowOptions { + is_resizable: false, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Unminimizable", move |_, cx| { + cx.open_window( + WindowOptions { + is_minimizable: false, + window_bounds: Some(window_bounds), + ..Default::default() + }, + |_, cx| { + cx.new(|_| SubWindow { + custom_titlebar: false, + is_dialog: false, + }) + }, + ) + .unwrap(); + })) + .child(button("Hide Application", |window, cx| { + cx.hide(); + + // Restore the application after 3 seconds + window + .spawn(cx, async move |cx| { + cx.background_executor() + .timer(std::time::Duration::from_secs(3)) + .await; + cx.update(|_, cx| { + cx.activate(false); + }) + }) + .detach(); + })) + .child(button("Resize", |window, _| { + let content_size = window.bounds().size; + window.resize(size(content_size.height, content_size.width)); + })) + .child(button("Prompt", |window, cx| { + let answer = window.prompt( + PromptLevel::Info, + "Are you sure?", + None, + &["OK", "Cancel"], + cx, + ); + + cx.spawn(async move |_| { + if answer.await.unwrap() == 0 { + println!("You have clicked Ok"); + } else { + println!("You have clicked Cancel"); + } + }) + .detach(); + })) + .child(button("Prompt (non-English)", |window, cx| { + let answer = window.prompt( + PromptLevel::Info, + "Are you sure?", + None, + &[PromptButton::ok("确定"), PromptButton::cancel("取消")], + cx, + ); + + cx.spawn(async move |_| { + if answer.await.unwrap() == 0 { + println!("You have clicked Ok"); + } else { + println!("You have clicked Cancel"); + } + }) + .detach(); + })) + } +} + +actions!(window, [Quit]); + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + cx.observe_window_bounds(window, move |_, window, _| { + println!("Window bounds changed: {:?}", window.bounds()); + }) + .detach(); + + WindowDemo {} + }) + }, + ) + .unwrap(); + + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/window_movable.rs b/vendor/gpui-pre/examples/window_movable.rs new file mode 100644 index 0000000..b5fb260 --- /dev/null +++ b/vendor/gpui-pre/examples/window_movable.rs @@ -0,0 +1,131 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, FocusHandle, Window, WindowBounds, WindowOptions, div, prelude::*, px, + rgb, size, +}; +use gpui::{SharedString, TitlebarOptions}; +use gpui_platform::application; + +struct ExampleWindow { + label: SharedString, + focus_handle: FocusHandle, +} + +impl Render for ExampleWindow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .track_focus(&self.focus_handle) + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x2e2e2e)) + .size_full() + .justify_center() + .items_center() + .p_8() + .text_lg() + .text_color(rgb(0xffffff)) + .child(self.label.clone()) + .child( + div() + .text_sm() + .text_color(rgb(0xb0b0b0)) + .child("Try to drag the titlebar, and check the Window menu."), + ) + } +} + +fn open_test_window( + cx: &mut App, + bounds: Bounds, + label: &str, + is_movable: bool, + appears_transparent: bool, + app_owns_titlebar_drag: bool, +) { + let label = SharedString::from(format!( + "{label}\nis_movable: {is_movable}\n\ + appears_transparent: {appears_transparent}\n\ + app_owns_titlebar_drag: {app_owns_titlebar_drag}" + )); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + is_movable, + app_owns_titlebar_drag, + titlebar: Some(TitlebarOptions { + title: Some(label.clone()), + appears_transparent, + ..Default::default() + }), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + ExampleWindow { + label, + focus_handle, + } + }) + }, + ) + .unwrap(); +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let window_size = size(px(420.), px(280.0)); + let base = Bounds::centered(None, window_size, cx); + + // (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) + let windows = [ + ("Native titlebar, movable", true, false, false, 0.0, 0.0), + ( + "Native titlebar, NOT movable", + false, + false, + false, + 1.0, + 0.0, + ), + ("Custom titlebar, movable", true, true, false, 0.0, 1.0), + ("Custom titlebar, NOT movable", false, true, false, 1.0, 1.0), + ]; + + for (label, is_movable, appears_transparent, app_owns_titlebar_drag, col, row) in windows { + let mut bounds = base; + bounds.origin.x += window_size.width * col; + bounds.origin.y += window_size.height * row; + open_test_window( + cx, + bounds, + label, + is_movable, + appears_transparent, + app_owns_titlebar_drag, + ); + } + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/window_positioning.rs b/vendor/gpui-pre/examples/window_positioning.rs new file mode 100644 index 0000000..22c9b35 --- /dev/null +++ b/vendor/gpui-pre/examples/window_positioning.rs @@ -0,0 +1,240 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, DisplayId, Hsla, Pixels, SharedString, Size, Window, + WindowBackgroundAppearance, WindowBounds, WindowKind, WindowOptions, div, point, prelude::*, + px, rgb, +}; +use gpui_platform::application; + +struct WindowContent { + text: SharedString, + bounds: Bounds, + bg: Hsla, +} + +impl Render for WindowContent { + fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { + let window_bounds = window.bounds(); + + div() + .flex() + .flex_col() + .bg(self.bg) + .size_full() + .items_center() + .text_color(rgb(0xffffff)) + .child(self.text.clone()) + .child( + div() + .flex() + .flex_col() + .text_sm() + .items_center() + .size_full() + .child(format!( + "origin: {}, {} size: {}, {}", + self.bounds.origin.x, + self.bounds.origin.y, + self.bounds.size.width, + self.bounds.size.height + )) + .child(format!( + "cx.bounds() origin: {}, {} size {}, {}", + window_bounds.origin.x, + window_bounds.origin.y, + window_bounds.size.width, + window_bounds.size.height + )), + ) + } +} + +fn build_window_options(display_id: DisplayId, bounds: Bounds) -> WindowOptions { + WindowOptions { + // Set the bounds of the window in screen coordinates + window_bounds: Some(WindowBounds::Windowed(bounds)), + // Specify the display_id to ensure the window is created on the correct screen + display_id: Some(display_id), + titlebar: None, + window_background: WindowBackgroundAppearance::Transparent, + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + app_id: None, + window_min_size: None, + window_decorations: None, + tabbing_identifier: None, + ..Default::default() + } +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + // Create several new windows, positioned in the top right corner of each screen + let size = Size { + width: px(350.), + height: px(75.), + }; + let margin_offset = px(150.); + + for screen in cx.displays() { + let bounds = Bounds { + origin: point(margin_offset, margin_offset), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Top Left {:?}", screen.id()).into(), + bg: gpui::red(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: screen.bounds().top_right() + - point(size.width + margin_offset, -margin_offset), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Top Right {:?}", screen.id()).into(), + bg: gpui::red(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: screen.bounds().bottom_left() + - point(-margin_offset, size.height + margin_offset), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Bottom Left {:?}", screen.id()).into(), + bg: gpui::blue(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: screen.bounds().bottom_right() + - point(size.width + margin_offset, size.height + margin_offset), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Bottom Right {:?}", screen.id()).into(), + bg: gpui::blue(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: point(screen.bounds().center().x - size.center().x, margin_offset), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Top Center {:?}", screen.id()).into(), + bg: gpui::black(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: point(margin_offset, screen.bounds().center().y - size.center().y), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Left Center {:?}", screen.id()).into(), + bg: gpui::black(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: point( + screen.bounds().center().x - size.center().x, + screen.bounds().center().y - size.center().y, + ), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Center {:?}", screen.id()).into(), + bg: gpui::black(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: point( + screen.bounds().size.width - size.width - margin_offset, + screen.bounds().center().y - size.center().y, + ), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Right Center {:?}", screen.id()).into(), + bg: gpui::black(), + bounds, + }) + }) + .unwrap(); + + let bounds = Bounds { + origin: point( + screen.bounds().center().x - size.center().x, + screen.bounds().size.height - size.height - margin_offset, + ), + size, + }; + + cx.open_window(build_window_options(screen.id(), bounds), |_, cx| { + cx.new(|_| WindowContent { + text: format!("Bottom Center {:?}", screen.id()).into(), + bg: gpui::black(), + bounds, + }) + }) + .unwrap(); + } + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/examples/window_shadow.rs b/vendor/gpui-pre/examples/window_shadow.rs new file mode 100644 index 0000000..fba5e91 --- /dev/null +++ b/vendor/gpui-pre/examples/window_shadow.rs @@ -0,0 +1,252 @@ +#![cfg_attr(target_family = "wasm", no_main)] + +#[path = "example_support/fonts.rs"] +mod example_support; + +use gpui::{ + App, Bounds, Context, CursorStyle, Decorations, HitboxBehavior, Hsla, MouseButton, Pixels, + Point, ResizeEdge, Size, Window, WindowBackgroundAppearance, WindowBounds, WindowDecorations, + WindowOptions, black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, + white, +}; +use gpui_platform::application; + +struct WindowShadow {} + +// Things to do: +// 1. We need a way of calculating which edge or corner the mouse is on, +// and then dispatch on that +// 2. We need to improve the shadow rendering significantly +// 3. We need to implement the techniques in here in Zed + +impl Render for WindowShadow { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let decorations = window.window_decorations(); + let rounding = px(10.0); + let shadow_size = px(10.0); + let border_size = px(1.0); + let grey = rgb(0x808080); + window.set_client_inset(shadow_size); + + div() + .id("window-backdrop") + .bg(transparent_black()) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling, .. } => div + .bg(gpui::transparent_black()) + .child( + canvas( + |_bounds, window, _cx| { + window.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + window.window_bounds().get_bounds().size, + ), + HitboxBehavior::Normal, + ) + }, + move |_bounds, hitbox, window, _cx| { + let mouse = window.mouse_position(); + let size = window.window_bounds().get_bounds().size; + let Some(edge) = resize_edge(mouse, shadow_size, size) else { + return; + }; + window.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => { + CursorStyle::ResizeUpDown + } + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(rounding) + }) + .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) + .when(!tiling.top, |div| div.pt(shadow_size)) + .when(!tiling.bottom, |div| div.pb(shadow_size)) + .when(!tiling.left, |div| div.pl(shadow_size)) + .when(!tiling.right, |div| div.pr(shadow_size)) + .on_mouse_move(|_e, window, _cx| window.refresh()) + .on_mouse_down(MouseButton::Left, move |e, window, _cx| { + let size = window.window_bounds().get_bounds().size; + let pos = e.position; + + match resize_edge(pos, shadow_size, size) { + Some(edge) => window.start_window_resize(edge), + None => window.start_window_move(), + }; + }), + }) + .size_full() + .child( + div() + .cursor(CursorStyle::Arrow) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .border_color(grey) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(rounding) + }) + .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) + .when(!tiling.top, |div| div.border_t(border_size)) + .when(!tiling.bottom, |div| div.border_b(border_size)) + .when(!tiling.left, |div| div.border_l(border_size)) + .when(!tiling.right, |div| div.border_r(border_size)) + .when(!tiling.is_tiled(), |div| { + div.shadow(vec![ + gpui::BoxShadow::new( + px(0.), + px(0.), + Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + ) + .blur_radius(shadow_size / 2.), + ]) + }), + }) + .on_mouse_move(|_e, _, cx| { + cx.stop_propagation(); + }) + .bg(gpui::rgb(0xCCCCFF)) + .size_full() + .flex() + .flex_col() + .justify_around() + .child( + div().w_full().flex().flex_row().justify_around().child( + div() + .flex() + .bg(white()) + .size(px(300.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child( + div() + .id("hello") + .w(px(200.0)) + .h(px(100.0)) + .bg(green()) + .shadow(vec![ + gpui::BoxShadow::new( + px(0.), + px(0.), + Hsla { + h: 0., + s: 0., + l: 0., + a: 1.0, + }, + ) + .blur_radius(px(20.0)), + ]) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { .. } => div + .on_mouse_down( + MouseButton::Left, + |_e, window, _| { + window.start_window_move(); + }, + ) + .on_click(|e, window, _| { + if e.is_right_click() { + window.show_window_menu(e.position()); + } + }) + .text_color(black()) + .child("this is the custom titlebar"), + }), + ), + ), + ), + ) + } +} + +fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { + let edge = if pos.y < shadow_size && pos.x < shadow_size { + ResizeEdge::TopLeft + } else if pos.y < shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::TopRight + } else if pos.y < shadow_size { + ResizeEdge::Top + } else if pos.y > size.height - shadow_size && pos.x < shadow_size { + ResizeEdge::BottomLeft + } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::BottomRight + } else if pos.y > size.height - shadow_size { + ResizeEdge::Bottom + } else if pos.x < shadow_size { + ResizeEdge::Left + } else if pos.x > size.width - shadow_size { + ResizeEdge::Right + } else { + return None; + }; + Some(edge) +} + +fn run_example() { + application().run(|cx: &mut App| { + if !example_support::load_fonts(cx) { + return; + } + let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_background: WindowBackgroundAppearance::Opaque, + window_decorations: Some(WindowDecorations::Client), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + cx.observe_window_appearance(window, |_, window, _| { + window.refresh(); + }) + .detach(); + WindowShadow {} + }) + }, + ) + .unwrap(); + }); +} + +#[cfg(not(target_family = "wasm"))] +fn main() { + run_example(); +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + run_example(); +} diff --git a/vendor/gpui-pre/resources/windows/gpui.manifest.xml b/vendor/gpui-pre/resources/windows/gpui.manifest.xml new file mode 100644 index 0000000..d11e1a4 --- /dev/null +++ b/vendor/gpui-pre/resources/windows/gpui.manifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2 + SegmentHeap + + + + + + + + diff --git a/vendor/gpui-pre/resources/windows/gpui.rc b/vendor/gpui-pre/resources/windows/gpui.rc new file mode 100644 index 0000000..ab20957 --- /dev/null +++ b/vendor/gpui-pre/resources/windows/gpui.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "gpui.manifest.xml" diff --git a/vendor/gpui-pre/src/_accessibility.rs b/vendor/gpui-pre/src/_accessibility.rs new file mode 100644 index 0000000..af14b63 --- /dev/null +++ b/vendor/gpui-pre/src/_accessibility.rs @@ -0,0 +1,295 @@ +//! # Accessibility in GPUI +//! +//! "Accessibility" refers to the ability of your application to be used by all +//! users, regardless of disability status. There are many aspects, all important, including: +//! - Ensuring sufficient text contrast. +//! - Providing a mechanism to disable animations. +//! - Providing a mechanism to increase text sizes. +//! - etc. +//! +//! This guide is focused on **programmatic accessibility**. This allows +//! assistive technology, such as screen readers or Braille displays, to inspect +//! and interact with your app. Docs for contributors working on accessibility +//! support can be found in the `a11y` module's doc comment. +//! +//! GPUI integrates with [AccessKit] to provide programmatic accessibility +//! features (referred to as simply "accessibility" for the rest of this guide). +//! +//! A minimal example can be found in the `examples/a11y` directory. +//! +//! ## Background +//! +//! Accessibility support is based on two key capabilities: +//! - Exposing information about the current UI state to assistive technology. +//! - Responding to actions requested by assistive technology. +//! +//! For example, a screen reader might want to announce to the user that a new +//! button has appeared. The user may then want to use a voice control program +//! to press that button. +//! +//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`] +//! +//! In GPUI, each [`Element`] can have an [`id`][Element::id]: +//! ```rust +//! # use gpui::*; +//! let div_with_id = div().id("my-id").child(text!("hello")); +//! +//! // IDs are optional +//! let div_without_id = div().child(text!("hello")); +//! ``` +//! +//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global +//! ID is formed by composing all the non-`None` IDs of its ancestors. For +//! example: +//! ```rust +//! # use gpui::*; +//! let inner = div().id("inner-id"); +//! let middle = div().child(inner); // no ID +//! let outer = div().id("outer-id").child(middle); +//! ``` +//! In this example, `inner`s global ID is (roughly speaking) `["outer-id", +//! "inner-id"]`. +//! +//! Since `middle` doesn't have an ID itself, it has no global ID. +//! +//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the +//! same frame will likely cause bugs. +//! +//! ### IDs and accessibility +//! +//! When GPUI renders a frame, it walks your UI tree, and finds nodes with +//! global IDs, and informs assistive technology about this node. +//! +//! In order for nodes to be reported, they must also have a non-`None` +//! [`role`][Element::a11y_role]. This is used to inform assistive technology +//! what *sort* of node it is (button, label, table, etc.). You can use +//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role. +//! +//! Nodes with the same global ID *across frames* are considered to be "the +//! same" node. For example: +//! ```rust +//! # use gpui::*; +//! // The UI in frame 1 +//! let frame_1 = div() +//! .id("parent") +//! .role(Role::Button) +//! .child( +//! div() +//! .id("id-1") +//! .role(Role::Label) +//! .child(text!("hello")) +//! ); +//! +//! // The UI on the next frame +//! let frame_2 = div() +//! .id("parent") +//! .role(Role::Button) +//! .child( +//! div() +//! .id("id-2") // <- different ID +//! .role(Role::Label) +//! .child(text!("hello")) +//! ); +//! ``` +//! Logically, the UI has not changed. But the screen reader has no way of +//! knowing that both child [`div`]s are "the same". So assistive technology +//! will interpret this as one node being removed, and another node being added. +//! This can be very disorienting for users, since announcements typically only +//! happen when something has *meaningfully* changed. +//! +//! In other words, by controlling the ID of an element, you can control whether +//! a change to a UI element is considered meaningful. You can also control +//! whether elements are reported to assistive technology *at all* by setting +//! the [`role`][Element::a11y_role], since nodes with no role are not reported. +//! +//! #### IDs and text +//! +//! Special care must be taken when dealing with text. +//! +//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type, +//! but automatically derives an ID. Usually, this is what you want. However, +//! the way it generates its ID is subtle and perhaps surprising. +//! +//! The ID of an invocation of the [`text!`] macro is derived from the +//! **location in the source code of that invocation**. For example: +//! +//! ```rust +//! # use gpui::*; +//! let a = text!("a"); +//! let b = text!("b"); +//! +//! // Different source locations, different IDs +//! assert_ne!(a.id(), b.id()); +//! +//! // However: +//! +//! fn make_text(s: &str) -> Text { text!(s) } +//! +//! let a = make_text("a"); +//! let b = make_text("b"); +//! +//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs +//! // are the same +//! assert_eq!(a.id(), b.id()); +//! ``` +//! This can produce surprising behaviour. For example, this footgun: +//! ```rust +//! # use gpui::*; +//! let todos = vec!["eat lunch", "drink water", "go to gym"]; +//! let todo_divs = todos.into_iter().map(|todo| { +//! text!(todo) +//! }); +//! +//! div() +//! .id("todo-list") +//! .role(Role::Document) +//! .children(todo_divs); // ERROR: multiple nodes with the same global ID +//! ``` +//! +//! Here, when we map the iterator, since we have only written [`text!`] once, +//! there is only one ID. And since they have the same ancestors and the same +//! ID, they will have the same global ID. In release builds, this will mean +//! some nodes get silently dropped! +//! +//! To fix this, you can set an ID: +//! ```rust +//! # use gpui::*; +//! let todos = vec!["eat lunch", "drink water", "go to gym"]; +//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| { +//! text!(todo).with_id(index) // OR `text(id = index, todo)` +//! }); +//! +//! div() +//! .id("todo-list") +//! .role(Role::Document) +//! .children(todo_divs); +//! ``` +//! Another possible solution is to wrap the [`text!`] in another node that +//! *does* have a unique global ID. For example: +//! ```rust +//! # use gpui::*; +//! let todos = vec!["eat lunch", "drink water", "go to gym"]; +//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| { +//! div().id(index).child(text!(todo)) +//! }); +//! +//! div() +//! .id("todo-list") +//! .role(Role::Document) +//! .children(todo_divs); +//! ``` +//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global +//! ID, and the global ID takes into account the IDs of all ancestors, this +//! works too. +//! +//! Occasionally, you will need to create a [`Text`] element with *no* ID. You +//! can achieve this with [`Text::new_inaccessible`]. If you are creating a +//! custom UI component (e.g. a button), you may want this so that you can set a +//! label property on a parent [`div`] without duplicating the text in the +//! accessibility tree. +//! +//! ### Handling actions +//! +//! Assistive technology can dispatch actions to the UI. While many users of +//! assistive technology use traditional input devices (e.g. a keyboard), some +//! use more specialized systems. For example, users with limited mobility may +//! use voice control to interact with your app. +//! +//! When a user dispatches an action, it is dispatched *to a specific node*. It +//! is your responsibility to tell the UI elements how they should respond when +//! a request comes in. +//! +//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait. +//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as +//! [`AccessibleAction`]. +//! +//! To respond to an accessible action, use +//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]: +//! ```rust,ignore +//! div() +//! .id("my-slider") +//! .role(Role::Slider) +//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| { +//! position += 1; +//! cx.notify(); +//! }) +//! .child(my_cool_slider()); +//! ``` +//! +//! Note that some common actions are automatically registered. For example, +//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an +//! [`AccessibleAction::Click`] handler that calls the click handler. +//! +//! ## Synthetic children +//! +//! Sometimes, a custom [`Element`] may want to appear as if it is really made +//! of multiple nodes. For example, a totally hypothetical custom text editor +//! element may want to have [`Role::TextInput`], while presenting children +//! consisting of [`Role::TextRun`]s. +//! +//! This is possible using [`Element::a11y_synthetic_children`]. For example: +//! ```rust,ignore +//! # use gpui::*; +//! impl Element for MyCustomTextField { +//! +//! // ... +//! +//! fn a11y_role(&self) -> Option { +//! Some(Role::TextInput) +//! } +//! +//! fn a11y_synthetic_children( +//! &mut self, +//! _prepaint: &mut Self::PrepaintState, +//! builder: &mut A11ySubtreeBuilder, +//! ) { +//! // Create the synthetic child node +//! let mut run = accesskit::Node::new(Role::TextRun); +//! run.set_value(self.text.clone()); +//! run.set_character_lengths( +//! self.text.chars().map(|c| c.len_utf8() as u8).collect::>(), +//! ); +//! +//! // Insert it as a child of `MyCustomTextField` +//! let run_id = builder.synthetic_node_id(0); +//! builder.push_child(run_id, run); +//! +//! // You can also mutate the parent (i.e. the `MyCustomTextField`) +//! let caret = accesskit::TextPosition { +//! node: run_id, +//! character_index: self.cursor, +//! }; +//! builder.parent_node().set_text_selection(accesskit::TextSelection { +//! anchor: caret, +//! focus: caret, +//! }); +//! } +//! } +//! ``` +//! +//! Notably, synthetic children are added *after* an element is +//! [prepainted][Element::prepaint], so prepaint state can be used (for example, +//! to determine what is visible on screen). +//! +//! ## Further reading +//! +//! Designing high-quality accessible interfaces can be challenging, in the same +//! way that designing high-quality traditional interfaces can be. The +//! following pages have useful information: +//! +//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses +//! internally. +//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and +//! states. +//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible +//! widgets. +//! +//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave +//! *exactly* as a web browser would with the same attributes. +//! +//! [AccessKit]: https://accesskit.dev/ +//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics +//! [apg]: https://www.w3.org/WAI/ARIA/apg/ + +#[cfg(doc)] +use crate::*; // so I don't have to qualify every type :) diff --git a/vendor/gpui-pre/src/_ownership_and_data_flow.rs b/vendor/gpui-pre/src/_ownership_and_data_flow.rs new file mode 100644 index 0000000..68699cc --- /dev/null +++ b/vendor/gpui-pre/src/_ownership_and_data_flow.rs @@ -0,0 +1,140 @@ +//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities. +//! +//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! gpui_platform::application().run(|cx: &mut App| { +//! let _counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! // ... +//! }); +//! ``` +//! +//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app. +//! +//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity: +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Context, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! gpui_platform::application().run(|cx: &mut App| { +//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! // Call `update` to access the model's state. +//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context| { +//! counter.count += 1; +//! }); +//! }); +//! ``` +//! +//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state. +//! +//! The callback is also provided a second `Context` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter. +//! +//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity}; +//! # struct Counter { +//! # count: usize, +//! # } +//! gpui_platform::application().run(|cx: &mut App| { +//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! counter.update(cx, |counter, cx| { +//! counter.count += 1; +//! cx.notify(); // Notify observers +//! }); +//! }); +//! ``` +//! +//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior. +//! +//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Entity, prelude::*}; +//! # struct Counter { +//! # count: usize, +//! # } +//! gpui_platform::application().run(|cx: &mut App| { +//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! +//! let second_counter = cx.new(|cx: &mut Context| { +//! // Note we can set up the callback before the Counter is even created! +//! cx.observe( +//! &first_counter, +//! |second: &mut Counter, first: Entity, cx| { +//! second.count = first.read(cx).count * 2; +//! }, +//! ) +//! .detach(); +//! +//! Counter { count: 0 } +//! }); +//! +//! first_counter.update(cx, |counter, cx| { +//! counter.count += 1; +//! cx.notify(); +//! }); +//! +//! assert_eq!(second_counter.read(cx).count, 2); +//! }); +//! ``` +//! +//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription. +//! +//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait. +//! +//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event: +//! +//! ```no_run +//! use gpui::EventEmitter; +//! # struct Counter { +//! # count: usize, +//! # } +//! struct CounterChangeEvent { +//! increment: usize, +//! } +//! +//! impl EventEmitter for Counter {} +//! ``` +//! +//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase. +//! +//! ```no_run +//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter}; +//! # struct Counter { +//! # count: usize, +//! # } +//! # struct CounterChangeEvent { +//! # increment: usize, +//! # } +//! # impl EventEmitter for Counter {} +//! gpui_platform::application().run(|cx: &mut App| { +//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); +//! +//! let second_counter = cx.new(|cx: &mut Context| { +//! // Note we can set up the callback before the Counter is even created! +//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity, event, _cx| { +//! second.count += event.increment * 2; +//! }) +//! .detach(); +//! +//! Counter { +//! count: first_counter.read(cx).count * 2, +//! } +//! }); +//! +//! first_counter.update(cx, |first, cx| { +//! first.count += 2; +//! cx.emit(CounterChangeEvent { increment: 2 }); +//! cx.notify(); +//! }); +//! +//! assert_eq!(second_counter.read(cx).count, 4); +//! }); +//! ``` diff --git a/vendor/gpui-pre/src/action.rs b/vendor/gpui-pre/src/action.rs new file mode 100644 index 0000000..f09d51c --- /dev/null +++ b/vendor/gpui-pre/src/action.rs @@ -0,0 +1,459 @@ +// Modified for gpui-pre (snapshot of zed@279fe07): the `actions!` derive paths are crate-relative. +use anyhow::{Context as _, Result}; +use collections::{HashMap, TypeIdHashMap}; +pub use gpui_macros::Action; +pub use no_action::{NoAction, Unbind, is_no_action, is_unbind}; +use serde_json::json; +use std::{ + any::{Any, TypeId}, + fmt::Display, +}; + +/// Defines and registers unit structs that can be used as actions. For more complex data types, derive `Action`. +/// +/// For example: +/// +/// ``` +/// use gpui::actions; +/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); +/// ``` +/// +/// This will create actions with names like `editor::MoveUp`, `editor::MoveDown`, etc. +/// +/// The namespace argument `editor` can also be omitted, though it is required for Zed actions. +#[macro_export] +macro_rules! actions { + ($namespace:path, [ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { + $( + #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)] + #[action(namespace = $namespace)] + $(#[$attr])* + pub struct $name; + )* + }; + ([ $( $(#[$attr:meta])* $name:ident),* $(,)? ]) => { + $( + #[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default, ::std::fmt::Debug, $crate::Action)] + $(#[$attr])* + pub struct $name; + )* + }; +} + +/// Actions are used to implement keyboard-driven UI. When you declare an action, you can bind keys +/// to the action in the keymap and listeners for that action in the element tree. +/// +/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit +/// struct action for each listed action name in the given namespace. +/// +/// ``` +/// use gpui::actions; +/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]); +/// ``` +/// +/// Registering the actions with the same name will result in a panic during `App` creation. +/// +/// # Derive Macro +/// +/// More complex data types can also be actions, by using the derive macro for `Action`: +/// +/// ``` +/// use gpui::Action; +/// #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, Action)] +/// #[action(namespace = editor)] +/// pub struct SelectNext { +/// pub replace_newest: bool, +/// } +/// ``` +/// +/// The derive macro for `Action` requires that the type implement `Clone` and `PartialEq`. It also +/// requires `serde::Deserialize` and `schemars::JsonSchema` unless `#[action(no_json)]` is +/// specified. In Zed these trait impls are used to load keymaps from JSON. +/// +/// Multiple arguments separated by commas may be specified in `#[action(...)]`: +/// +/// - `namespace = some_namespace` sets the namespace. In Zed this is required. +/// +/// - `name = "ActionName"` overrides the action's name. This must not contain `::`. +/// +/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`, +/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`. +/// +/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait +/// while not supporting invocation by name or JSON deserialization. +/// +/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action. +/// These action names should *not* correspond to any actions that are registered. These old names +/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will +/// accept these old names and provide warnings. +/// +/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message. +/// In Zed, the keymap JSON schema will cause this to be displayed as a warning. +/// +/// # Manual Implementation +/// +/// If you want to control the behavior of the action trait manually, you can use the lower-level +/// `#[register_action]` macro, which only generates the code needed to register your action before +/// `main`. +/// +/// ``` +/// use gpui::{SharedString, register_action}; +/// #[derive(Clone, PartialEq, Eq, serde::Deserialize, schemars::JsonSchema)] +/// pub struct Paste { +/// pub content: SharedString, +/// } +/// +/// impl gpui::Action for Paste { +/// # fn boxed_clone(&self) -> Box { unimplemented!()} +/// # fn partial_eq(&self, other: &dyn gpui::Action) -> bool { unimplemented!() } +/// # fn name(&self) -> &'static str { "Paste" } +/// # fn name_for_type() -> &'static str { "Paste" } +/// # fn build(value: serde_json::Value) -> anyhow::Result> { +/// # unimplemented!() +/// # } +/// } +/// +/// register_action!(Paste); +/// ``` +pub trait Action: Any + Send { + /// Clone the action into a new box + fn boxed_clone(&self) -> Box; + + /// Do a partial equality check on this action and the other + fn partial_eq(&self, action: &dyn Action) -> bool; + + /// Get the name of this action, for displaying in UI + fn name(&self) -> &'static str; + + /// Get the name of this action type (static) + fn name_for_type() -> &'static str + where + Self: Sized; + + /// Build this action from a JSON value. This is used to construct actions from the keymap. + /// A value of `{}` will be passed for actions that don't have any parameters. + fn build(value: serde_json::Value) -> Result> + where + Self: Sized; + + /// Optional JSON schema for the action's input data. + fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option + where + Self: Sized, + { + None + } + + /// A list of alternate, deprecated names for this action. These names can still be used to + /// invoke the action. In Zed, the keymap JSON schema will accept these old names and provide + /// warnings. + fn deprecated_aliases() -> &'static [&'static str] + where + Self: Sized, + { + &[] + } + + /// Returns the deprecation message for this action, if any. In Zed, the keymap JSON schema will + /// cause this to be displayed as a warning. + fn deprecation_message() -> Option<&'static str> + where + Self: Sized, + { + None + } + + /// The documentation for this action, if any. When using the derive macro for actions + /// this will be automatically generated from the doc comments on the action struct. + fn documentation() -> Option<&'static str> + where + Self: Sized, + { + None + } +} + +impl std::fmt::Debug for dyn Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dyn Action") + .field("name", &self.name()) + .finish() + } +} + +impl dyn Action { + /// Type-erase Action type. + pub fn as_any(&self) -> &dyn Any { + self as &dyn Any + } +} + +/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use +/// markdown to display it. +#[derive(Debug)] +pub enum ActionBuildError { + /// Indicates that an action with this name has not been registered. + NotFound { + /// Name of the action that was not found. + name: String, + }, + /// Indicates that an error occurred while building the action, typically a JSON deserialization + /// error. + BuildError { + /// Name of the action that was attempting to be built. + name: String, + /// Error that occurred while building the action. + error: anyhow::Error, + }, +} + +impl std::error::Error for ActionBuildError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ActionBuildError::NotFound { .. } => None, + ActionBuildError::BuildError { error, .. } => error.source(), + } + } +} + +impl Display for ActionBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActionBuildError::NotFound { name } => { + write!(f, "Didn't find an action named \"{name}\"") + } + ActionBuildError::BuildError { name, error } => { + write!(f, "Error while building action \"{name}\": {error}") + } + } + } +} + +type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; + +pub(crate) struct ActionRegistry { + by_name: HashMap<&'static str, ActionData>, + names_by_type_id: TypeIdHashMap<&'static str>, + all_names: Vec<&'static str>, // So we can return a static slice. + deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name + deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message + documentation: HashMap<&'static str, &'static str>, // action name -> documentation +} + +impl Default for ActionRegistry { + fn default() -> Self { + let mut this = ActionRegistry { + by_name: Default::default(), + names_by_type_id: Default::default(), + documentation: Default::default(), + all_names: Default::default(), + deprecated_aliases: Default::default(), + deprecation_messages: Default::default(), + }; + + this.load_actions(); + + this + } +} + +struct ActionData { + pub build: ActionBuilder, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, +} + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct MacroActionBuilder(pub fn() -> MacroActionData); + +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct MacroActionData { + pub name: &'static str, + pub type_id: TypeId, + pub build: ActionBuilder, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, + pub deprecated_aliases: &'static [&'static str], + pub deprecation_message: Option<&'static str>, + pub documentation: Option<&'static str>, +} + +inventory::collect!(MacroActionBuilder); + +impl ActionRegistry { + /// Load all registered actions into the registry. + pub(crate) fn load_actions(&mut self) { + for builder in inventory::iter:: { + let action = builder.0(); + self.insert_action(action); + } + } + + fn insert_action(&mut self, action: MacroActionData) { + let name = action.name; + if self.by_name.contains_key(name) { + panic!( + "Action with name `{name}` already registered \ + (might be registered in `#[action(deprecated_aliases = [...])]`." + ); + } + self.by_name.insert( + name, + ActionData { + build: action.build, + json_schema: action.json_schema, + }, + ); + for &alias in action.deprecated_aliases { + if self.by_name.contains_key(alias) { + panic!( + "Action with name `{alias}` already registered. \ + `{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`." + ); + } + self.by_name.insert( + alias, + ActionData { + build: action.build, + json_schema: action.json_schema, + }, + ); + self.deprecated_aliases.insert(alias, name); + self.all_names.push(alias); + } + self.names_by_type_id.insert(action.type_id, name); + self.all_names.push(name); + if let Some(deprecation_msg) = action.deprecation_message { + self.deprecation_messages.insert(name, deprecation_msg); + } + if let Some(documentation) = action.documentation { + self.documentation.insert(name, documentation); + } + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action_type(&self, type_id: &TypeId) -> Result> { + let name = self + .names_by_type_id + .get(type_id) + .with_context(|| format!("no action type registered for {type_id:?}"))?; + + Ok(self.build_action(name, None)?) + } + + #[cfg(feature = "profiler")] + pub(crate) fn try_resolve_action(&self, type_id: &TypeId) -> Option<&'static str> { + self.names_by_type_id.get(type_id).copied() + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action( + &self, + name: &str, + params: Option, + ) -> std::result::Result, ActionBuildError> { + let build_action = self + .by_name + .get(name) + .ok_or_else(|| ActionBuildError::NotFound { + name: name.to_owned(), + })? + .build; + (build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| { + ActionBuildError::BuildError { + name: name.to_owned(), + error: e, + } + }) + } + + pub fn all_action_names(&self) -> &[&'static str] { + self.all_names.as_slice() + } + + pub fn action_schemas( + &self, + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { + // Use the order from all_names so that the resulting schema has sensible order. + self.all_names + .iter() + .map(|name| { + let action_data = self + .by_name + .get(name) + .expect("All actions in all_names should be registered"); + (*name, (action_data.json_schema)(generator)) + }) + .collect::>() + } + + pub fn action_schema_by_name( + &self, + name: &str, + generator: &mut schemars::SchemaGenerator, + ) -> Option> { + self.by_name + .get(name) + .map(|action_data| (action_data.json_schema)(generator)) + } + + pub fn deprecated_aliases(&self) -> &HashMap<&'static str, &'static str> { + &self.deprecated_aliases + } + + pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { + &self.deprecation_messages + } + + pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { + &self.documentation + } +} + +/// Generate a list of all the registered actions. +/// Useful for transforming the list of available actions into a +/// format suited for static analysis such as in validating keymaps, or +/// generating documentation. +pub fn generate_list_of_all_registered_actions() -> impl Iterator { + inventory::iter:: + .into_iter() + .map(|builder| builder.0()) +} + +mod no_action { + use crate as gpui; + use schemars::JsonSchema; + use serde::Deserialize; + + actions!( + zed, + [ + /// Action with special handling which unbinds the keybinding this is associated with, + /// if it is the highest precedence match. + NoAction + ] + ); + + /// Action with special handling which unbinds later bindings for the same keystrokes when they + /// dispatch the named action, regardless of that action's context. + /// + /// In keymap JSON this is written as: + /// + /// `["zed::Unbind", "editor::NewLine"]` + #[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, gpui::Action)] + #[action(namespace = zed)] + pub struct Unbind(pub gpui::SharedString); + + /// Returns whether or not this action represents a removed key binding. + pub fn is_no_action(action: &dyn gpui::Action) -> bool { + action.as_any().is::() + } + + /// Returns whether or not this action represents an unbind marker. + pub fn is_unbind(action: &dyn gpui::Action) -> bool { + action.as_any().is::() + } +} diff --git a/vendor/gpui-pre/src/app.rs b/vendor/gpui-pre/src/app.rs new file mode 100644 index 0000000..a921de1 --- /dev/null +++ b/vendor/gpui-pre/src/app.rs @@ -0,0 +1,3607 @@ +use scheduler::Instant; +use std::{ + any::{TypeId, type_name}, + cell::{BorrowMutError, Cell, Ref, RefCell, RefMut}, + ffi::OsString, + marker::PhantomData, + mem, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{Arc, atomic::Ordering::SeqCst}, + time::Duration, +}; + +use anyhow::{Context as _, Result, anyhow}; +use derive_more::{Deref, DerefMut}; +use futures::{Future, FutureExt, channel::oneshot, future::LocalBoxFuture}; +use itertools::Itertools; +use parking_lot::RwLock; +use slotmap::SlotMap; + +pub use async_context::*; +#[cfg(feature = "bench-support")] +pub use bench_context::{ + BenchAppContext, BenchMeasurement, BenchReport, BenchWindowContext, CountingAllocator, + MetricReport, bench_platform, +}; +use collections::{FxHashMap, FxHashSet, HashMap, TypeIdHashMap, TypeIdHashSet, VecDeque}; +pub use context::*; +pub use entity_map::*; +use gpui_util::{ResultExt, debug_panic}; +#[cfg(any(test, feature = "test-support"))] +pub use headless_app_context::*; +use http_client::{HttpClient, Url}; +use smallvec::SmallVec; +#[cfg(any(test, feature = "test-support"))] +pub use test_app::*; +#[cfg(any(test, feature = "test-support"))] +pub use test_context::*; +#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] +pub use visual_test_context::*; + +#[cfg(any(feature = "inspector", debug_assertions))] +use crate::InspectorElementRegistry; +use crate::asset_cache::CachedLoad; +use crate::{ + Action, ActionBuildError, ActionRegistry, ActivationPolicy, ActivityGuard, Any, AnyView, + AnyWindowHandle, AppContext, Arena, ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, + ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase, DisplayId, EventEmitter, + ExternalDragPayload, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, + Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, + Priority, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, + RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet, + Subscription, SvgRenderer, SystemNotification, SystemNotificationResponse, Task, + TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowButtonLayout, + WindowHandle, WindowId, WindowInvalidator, WindowingRequest, + colors::{Colors, GlobalColors}, + hash, init_app_menus, +}; + +mod async_context; +#[cfg(feature = "bench-support")] +mod bench_context; +mod context; +mod entity_map; +#[cfg(any(test, feature = "test-support"))] +mod headless_app_context; +#[cfg(any(test, feature = "test-support"))] +mod test_app; +#[cfg(any(test, feature = "test-support"))] +mod test_context; +#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] +mod visual_test_context; + +/// The duration for which native applications wait for futures returned from +/// [Context::on_app_quit] before fully quitting. +pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(200); + +/// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. +/// Strongly consider removing after stabilization. +#[doc(hidden)] +pub struct AppCell { + app: RefCell, +} + +impl AppCell { + #[doc(hidden)] + #[track_caller] + pub fn borrow(&self) -> AppRef<'_> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRef(self.app.borrow()) + } + + #[doc(hidden)] + #[track_caller] + pub fn borrow_mut(&self) -> AppRefMut<'_> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + AppRefMut(self.app.borrow_mut()) + } + + #[doc(hidden)] + #[track_caller] + pub fn try_borrow_mut(&self) -> Result, BorrowMutError> { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("borrowed {thread_id:?}"); + } + Ok(AppRefMut(self.app.try_borrow_mut()?)) + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRef<'a>(Ref<'a, App>); + +impl Drop for AppRef<'_> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped borrow from {thread_id:?}"); + } + } +} + +#[doc(hidden)] +#[derive(Deref, DerefMut)] +pub struct AppRefMut<'a>(RefMut<'a, App>); + +impl Drop for AppRefMut<'_> { + fn drop(&mut self) { + if option_env!("TRACK_THREAD_BORROWS").is_some() { + let thread_id = std::thread::current().id(); + eprintln!("dropped {thread_id:?}"); + } + } +} + +/// A reference to a GPUI application, typically constructed in the `main` function of your app. +/// You won't interact with this type much outside of initial configuration and startup. +pub struct Application(Rc); + +/// A strong handle to an [`Application`] started with [`Application::run_embedded`]. +/// +/// Dropping this handle releases the app, so an embedder must hold it for as long as the +/// app should run. While held, it is the embedder's entry point back into GPUI each time +/// the external run loop gives it control. +pub struct ApplicationHandle { + app: Rc, +} + +impl ApplicationHandle { + /// Invoke `f` with the app context. Must not be called re-entrantly from code that + /// is already inside an update; the app state is a `RefCell` and will panic on a + /// double borrow. + pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { + let cx = &mut *self.app.borrow_mut(); + f(cx) + } + + /// An [`AsyncApp`] for use across await points. It holds the app weakly; keeping the + /// app alive remains this handle's job. + pub fn to_async(&self) -> AsyncApp { + self.update(|cx| cx.to_async()) + } +} + +/// Represents an application before it is fully launched. Once your app is +/// configured, you'll start the app with `App::run`. +impl Application { + /// Builds an app with a caller-provided platform implementation. + pub fn with_platform(platform: Rc) -> Self { + Self(App::new_app( + platform, + Arc::new(()), + Arc::new(NullHttpClient), + )) + } + + /// Builds an app with accessibility (AccessKit) integration forcibly + /// disabled. + /// + /// In this mode, accessibility APIs (e.g. + /// [`div().role()`][crate::StatefulInteractiveElement::role]) silently + /// no-op. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview of + /// the features this disables. + pub fn new_inaccessible(platform: Rc) -> Self { + let this = Self::with_platform(platform); + this.0.borrow_mut().accessibility_force_disabled = true; + this + } + + /// Assigns the source of assets for the application. + pub fn with_assets(self, asset_source: impl AssetSource) -> Self { + let mut context_lock = self.0.borrow_mut(); + let asset_source = Arc::new(asset_source); + context_lock.asset_source = asset_source.clone(); + context_lock.svg_renderer = SvgRenderer::new(asset_source); + drop(context_lock); + self + } + + /// Configures arguments to pass when restarting the application. + pub fn with_restart_arguments(self, arguments: Vec) -> Self { + self.0.borrow_mut().restart_arguments = arguments; + self + } + + /// Sets the HTTP client for the application. + pub fn with_http_client(self, http_client: Arc) -> Self { + let mut context_lock = self.0.borrow_mut(); + context_lock.http_client = http_client; + drop(context_lock); + self + } + + /// Configures when the application should automatically quit. + /// By default, [`QuitMode::Default`] is used. + pub fn with_quit_mode(self, mode: QuitMode) -> Self { + self.0.borrow_mut().quit_mode = mode; + self + } + + /// Sets the windowing mode the app starts in. See [`App::request_windowing`]. + /// + /// Defaults to windowed. On Linux, the default environment is the process's own, and the app + /// starts headless if that names no allowed display server. On macOS, headless means the app + /// starts without a Dock icon or menu bar ([`ActivationPolicy::Accessory`]). Has no effect on + /// other platforms. + pub fn with_windowing(self, request: WindowingRequest) -> Self { + self.0.borrow().platform.set_initial_windowing(request); + self + } + + /// Start the application. The provided callback will be called once the + /// app is fully launched. + /// + /// On WebAssembly, this returns immediately and retains the app for the lifetime + /// of the Wasm instance. Use [`Self::run_embedded`] to control its lifetime explicitly. + pub fn run(self, on_finish_launching: F) + where + F: 'static + FnOnce(&mut App), + { + let this = self.0.clone(); + let platform = self.0.borrow().platform.clone(); + platform.run(Box::new(move || { + let cx = &mut *this.borrow_mut(); + on_finish_launching(cx); + })); + + #[cfg(target_family = "wasm")] + std::mem::forget(self); + } + + /// Start the application for an embedder that drives the run loop itself. + /// + /// On ordinary platforms `Platform::run` blocks for the lifetime of the app, and the + /// app state is kept alive by [`Application::run`]'s stack frame. Embedded platforms — + /// where the run loop belongs to someone else, e.g. GPUI compiled into a Wasm guest, + /// or a GPUI view hosted inside a foreign native application — implement + /// `Platform::run` to invoke the launch callback and return immediately. This method + /// supports that shape: it returns an [`ApplicationHandle`] that keeps the app alive + /// and lets the embedder re-enter it whenever the external run loop yields control. + pub fn run_embedded(self, on_finish_launching: F) -> ApplicationHandle + where + F: 'static + FnOnce(&mut App), + { + let this = self.0.clone(); + let platform = self.0.borrow().platform.clone(); + platform.run(Box::new(move || { + let cx = &mut *this.borrow_mut(); + on_finish_launching(cx); + })); + ApplicationHandle { app: self.0 } + } + + /// Register a handler to be invoked when the platform instructs the application + /// to open one or more URLs. + pub fn on_open_urls(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(Vec), + { + self.0.borrow().platform.on_open_urls(Box::new(callback)); + self + } + + /// Invokes a handler when an already-running application is launched. + /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock. + pub fn on_reopen(&self, mut callback: F) -> &Self + where + F: 'static + FnMut(&mut App), + { + let this = Rc::downgrade(&self.0); + self.0.borrow_mut().platform.on_reopen(Box::new(move || { + if let Some(app) = this.upgrade() { + callback(&mut app.borrow_mut()); + } + })); + self + } + + /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background. + pub fn background_executor(&self) -> BackgroundExecutor { + self.0.borrow().background_executor.clone() + } + + /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> ForegroundExecutor { + self.0.borrow().foreground_executor.clone() + } + + /// Returns a reference to the [`TextSystem`] associated with this app. + pub fn text_system(&self) -> Arc { + self.0.borrow().text_system.clone() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.0.borrow().path_for_auxiliary_executable(name) + } +} + +type Handler = Box bool + 'static>; +type Listener = Box bool + 'static>; +type MissingGlyphCallback = Box; +pub(crate) type KeystrokeObserver = + Box bool + 'static>; +type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; +type WindowClosedHandler = Box; +type ReleaseListener = Box; +type NewEntityListener = Box, &mut App) + 'static>; + +struct MissingGlyphCallbackEntry { + registration: Rc<()>, + callback: Option, +} + +#[derive(Default)] +struct MissingGlyphCallbackSlot { + entry: RefCell>, +} + +impl MissingGlyphCallbackSlot { + fn replace(&self, callback: MissingGlyphCallback) -> Rc<()> { + let registration = Rc::new(()); + self.entry.borrow_mut().replace(MissingGlyphCallbackEntry { + registration: registration.clone(), + callback: Some(callback), + }); + registration + } + + fn invoke(&self, missing_glyphs: &[MissingGlyph], cx: &mut App) { + let Some((registration, mut callback)) = + self.entry.borrow_mut().as_mut().and_then(|entry| { + entry + .callback + .take() + .map(|callback| (entry.registration.clone(), callback)) + }) + else { + return; + }; + callback(missing_glyphs, cx); + + let mut entry = self.entry.borrow_mut(); + let is_current = entry + .as_ref() + .is_some_and(|entry| Rc::ptr_eq(&entry.registration, ®istration)); + if is_current { + let Some(entry) = entry.as_mut() else { + return; + }; + entry.callback = Some(callback); + } + } + + fn remove(&self, registration: &Rc<()>) -> bool { + let mut entry = self.entry.borrow_mut(); + let is_current = entry + .as_ref() + .is_some_and(|entry| Rc::ptr_eq(&entry.registration, registration)); + if is_current { + entry.take(); + } + is_current + } +} + +/// Defines when the application should automatically quit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum QuitMode { + /// Use [`QuitMode::Explicit`] on macOS and [`QuitMode::LastWindowClosed`] on other platforms. + #[default] + Default, + /// Quit automatically when the last window is closed. + LastWindowClosed, + /// Quit only when requested via [`App::quit`]. + Explicit, +} + +/// Controls when GPUI hides the mouse cursor in response to keyboard input. +/// +/// Restoration on mouse motion is handled by the platform layer; this enum +/// only describes the policy for *triggering* a hide. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum CursorHideMode { + /// Never hide the cursor automatically. + Never, + /// Hide on character-producing key presses (typing). + OnTyping, + /// Hide on character-producing key presses, *and* when a key binding + /// resolves to an action that consumes the keystroke. + #[default] + OnTypingAndAction, +} + +#[doc(hidden)] +#[derive(Clone, PartialEq, Eq)] +pub struct SystemWindowTab { + pub id: WindowId, + pub title: SharedString, + pub handle: AnyWindowHandle, + pub last_active_at: Instant, +} + +impl SystemWindowTab { + /// Create a new instance of the window tab. + pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self { + Self { + id: handle.id, + title, + handle, + last_active_at: Instant::now(), + } + } +} + +/// A controller for managing window tabs. +#[derive(Default)] +pub struct SystemWindowTabController { + visible: Option, + tab_groups: FxHashMap>, +} + +impl Global for SystemWindowTabController {} + +impl SystemWindowTabController { + /// Create a new instance of the window tab controller. + pub fn new() -> Self { + Self { + visible: None, + tab_groups: FxHashMap::default(), + } + } + + /// Initialize the global window tab controller. + pub fn init(cx: &mut App) { + cx.set_global(SystemWindowTabController::new()); + } + + /// Get all tab groups. + pub fn tab_groups(&self) -> &FxHashMap> { + &self.tab_groups + } + + /// Get the next tab group window handle. + pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + // TODO: `.keys()` returns arbitrary order, what does "next" mean? + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let next_idx = (idx + 1) % group_ids.len(); + + controller + .tab_groups + .get(group_ids[next_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get the previous tab group window handle. + pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> { + let controller = cx.global::(); + let current_group = controller + .tab_groups + .iter() + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group)); + + let current_group = current_group?; + // TODO: `.keys()` returns arbitrary order, what does "previous" mean? + let mut group_ids: Vec<_> = controller.tab_groups.keys().collect(); + let idx = group_ids.iter().position(|g| *g == current_group)?; + let prev_idx = if idx == 0 { + group_ids.len() - 1 + } else { + idx - 1 + }; + + controller + .tab_groups + .get(group_ids[prev_idx]) + .and_then(|tabs| { + tabs.iter() + .max_by_key(|tab| tab.last_active_at) + .or_else(|| tabs.first()) + .map(|tab| &tab.handle) + }) + } + + /// Get all tabs in the same window. + pub fn tabs(&self, id: WindowId) -> Option<&Vec> { + self.tab_groups + .values() + .find(|tabs| tabs.iter().any(|tab| tab.id == id)) + } + + /// Initialize the visibility of the system window tab controller. + pub fn init_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + if controller.visible.is_none() { + controller.visible = Some(visible); + } + } + + /// Get the visibility of the system window tab controller. + pub fn is_visible(&self) -> bool { + self.visible.unwrap_or(false) + } + + /// Set the visibility of the system window tab controller. + pub fn set_visible(cx: &mut App, visible: bool) { + let mut controller = cx.global_mut::(); + controller.visible = Some(visible); + } + + /// Update the last active of a window. + pub fn update_last_active(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.last_active_at = Instant::now(); + } + } + } + } + + /// Update the position of a tab within its group. + pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) { + let mut controller = cx.global_mut::(); + for (_, windows) in controller.tab_groups.iter_mut() { + if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) { + if ix < windows.len() && current_pos != ix { + let window_tab = windows.remove(current_pos); + windows.insert(ix, window_tab); + } + break; + } + } + } + + /// Update the title of a tab. + pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) { + let controller = cx.global::(); + let tab = controller + .tab_groups + .values() + .flat_map(|windows| windows.iter()) + .find(|tab| tab.id == id); + + if tab.map_or(true, |t| t.title == title) { + return; + } + + let mut controller = cx.global_mut::(); + for windows in controller.tab_groups.values_mut() { + for tab in windows.iter_mut() { + if tab.id == id { + tab.title = title; + return; + } + } + } + } + + /// Insert a tab into a tab group. + pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec) { + let mut controller = cx.global_mut::(); + let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else { + return; + }; + + let mut expected_tab_ids: Vec<_> = tabs + .iter() + .filter(|tab| tab.id != id) + .map(|tab| tab.id) + .sorted() + .collect(); + + let mut tab_group_id = None; + for (group_id, group_tabs) in &controller.tab_groups { + let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect(); + if tab_ids == expected_tab_ids { + tab_group_id = Some(*group_id); + break; + } + } + + if let Some(tab_group_id) = tab_group_id { + if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) { + tabs.push(tab); + } + } else { + let new_group_id = controller.tab_groups.len(); + controller.tab_groups.insert(new_group_id, tabs); + } + } + + /// Remove a tab from a tab group. + pub fn remove_tab(cx: &mut App, id: WindowId) -> Option { + let mut controller = cx.global_mut::(); + let mut removed_tab = None; + + controller.tab_groups.retain(|_, tabs| { + if let Some(pos) = tabs.iter().position(|tab| tab.id == id) { + removed_tab = Some(tabs.remove(pos)); + } + !tabs.is_empty() + }); + + removed_tab + } + + /// Move a tab to a new tab group. + pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) { + let mut removed_tab = Self::remove_tab(cx, id); + let mut controller = cx.global_mut::(); + + if let Some(tab) = removed_tab { + let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1); + controller.tab_groups.insert(new_group_id, vec![tab]); + } + } + + /// Merge all tab groups into a single group. + pub fn merge_all_windows(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(initial_tabs) = controller.tabs(id) else { + return; + }; + + let initial_tabs_len = initial_tabs.len(); + let mut all_tabs = initial_tabs.clone(); + + for (_, mut tabs) in controller.tab_groups.drain() { + tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab)); + all_tabs.extend(tabs); + } + + controller.tab_groups.insert(0, all_tabs); + } + + /// Selects the next tab in the tab group in the trailing direction. + pub fn select_next_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let next_index = (current_index + 1) % tabs.len(); + + let _ = &tabs[next_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } + + /// Selects the previous tab in the tab group in the leading direction. + pub fn select_previous_tab(cx: &mut App, id: WindowId) { + let mut controller = cx.global_mut::(); + let Some(tabs) = controller.tabs(id) else { + return; + }; + + let current_index = tabs.iter().position(|tab| tab.id == id).unwrap(); + let previous_index = if current_index == 0 { + tabs.len() - 1 + } else { + current_index - 1 + }; + + let _ = &tabs[previous_index].handle.update(cx, |_, window, _| { + window.activate_window(); + }); + } +} + +pub(crate) enum GpuiMode { + #[cfg(any(test, feature = "test-support"))] + Test { + skip_drawing: bool, + }, + Production, +} + +impl GpuiMode { + #[cfg(any(test, feature = "test-support"))] + pub fn test() -> Self { + GpuiMode::Test { + skip_drawing: false, + } + } + + #[inline] + pub(crate) fn skip_drawing(&self) -> bool { + match self { + #[cfg(any(test, feature = "test-support"))] + GpuiMode::Test { skip_drawing } => *skip_drawing, + GpuiMode::Production => false, + } + } +} + +struct PlatformOwnedDrag { + source_window: WindowId, + state: PlatformOwnedDragState, +} + +enum PlatformOwnedDragState { + Suspended(AnyDrag), + // A source-window drop consumes `active_drag` before AppKit ends the dragging session, so this + // marker can outlive the active drag and is cleaned up by `FileDropEvent::Ended`. + RestoredInSourceWindow, +} + +/// Contains the state of the full application, and passed as a reference to a variety of callbacks. +/// Other [Context] derefs to this type. +/// You need a reference to an `App` to access the state of a [Entity]. +pub struct App { + pub(crate) this: Weak, + pub(crate) platform: Rc, + text_system: Arc, + + pub(crate) actions: Rc, + pub(crate) active_drag: Option, + platform_owned_drag: Option, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, + #[cfg(feature = "profiler")] + foreground_journal: crate::profiler::journal::ForegroundJournal, + #[cfg(all(feature = "profiler", not(target_family = "wasm")))] + hang_monitor: Option, + pub(crate) entities: EntityMap, + pub(crate) new_entity_observers: SubscriberSet, + pub(crate) windows: SlotMap>>, + pub(crate) window_handles: FxHashMap, + pub(crate) focus_handles: Arc, + pub(crate) keymap: Rc>, + pub(crate) keyboard_layout: Box, + pub(crate) keyboard_mapper: Rc, + pub(crate) global_action_listeners: + TypeIdHashMap>>, + pending_effects: VecDeque, + + pub(crate) observers: SubscriberSet, + pub(crate) event_listeners: SubscriberSet, + pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, + pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>, + pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>, + missing_glyph_callback: Rc, + pub(crate) thermal_state_observers: SubscriberSet<(), Handler>, + pub(crate) system_sleep_observers: SubscriberSet<(), Handler>, + pub(crate) system_wake_observers: SubscriberSet<(), Handler>, + pub(crate) release_listeners: SubscriberSet, + pub(crate) global_observers: SubscriberSet, + pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, + pub(crate) restart_observers: SubscriberSet<(), Handler>, + pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, + + /// Per-App element arena. This isolates element allocations between different + /// App instances (important for tests where multiple Apps run concurrently). + pub(crate) element_arena: RefCell, + /// Per-App event arena. + pub(crate) event_arena: Arena, + + // Drop globals last. We need to ensure all tasks owned by entities and + // callbacks are marked cancelled at this point as this will also shutdown + // the tokio runtime. As any task attempting to spawn a blocking tokio task, + // might panic. + pub(crate) globals_by_type: TypeIdHashMap>, + + // assets + pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, + asset_source: Arc, + pub(crate) svg_renderer: SvgRenderer, + http_client: Arc, + + // below is plain data, the drop order is insignificant here + pub(crate) pending_notifications: FxHashSet, + pub(crate) pending_global_notifications: TypeIdHashSet, + pub(crate) restart_path: Option, + pub(crate) restart_arguments: Vec, + pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. + pub(crate) propagate_event: bool, + pub(crate) prompt_builder: Option, + pub(crate) window_invalidators_by_entity: + FxHashMap>, + pub(crate) tracked_entities: FxHashMap>, + pub(crate) current_window_by_entity: FxHashMap, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_renderer: Option, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_element_registry: InspectorElementRegistry, + #[cfg(any(test, feature = "test-support", debug_assertions))] + pub(crate) name: Option<&'static str>, + pub(crate) text_rendering_mode: Rc>, + + pub(crate) window_update_stack: Vec, + pub(crate) mode: GpuiMode, + pub(crate) cursor_hide_mode: CursorHideMode, + pub(crate) reduce_motion: bool, + /// Origin of the shared clock that phase-locks synced repeating animations. + pub(crate) synced_animation_epoch: Instant, + /// Whether the app was created by [`Application::new_inaccessible`]. No + /// accesskit APIs will be called when this flag is set. + pub(crate) accessibility_force_disabled: bool, + flushing_effects: bool, + pending_updates: usize, + quit_mode: QuitMode, + quitting: bool, + + // We need to ensure the leak detector drops last, after all tasks, callbacks and things have been dropped. + // Otherwise it may report false positives. + #[cfg(any(test, gpui_leak_detection))] + _ref_counts: Arc>, +} + +impl App { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new_app( + platform: Rc, + asset_source: Arc, + http_client: Arc, + ) -> Rc { + let background_executor = platform.background_executor(); + let foreground_executor = platform.foreground_executor(); + assert!( + background_executor.is_main_thread(), + "must construct App on main thread" + ); + #[cfg(feature = "profiler")] + let foreground_journal = crate::profiler::journal::install_foreground_journal(); + let synced_animation_epoch = background_executor.now(); + + let text_system = Arc::new(TextSystem::new(platform.text_system())); + let entities = EntityMap::new(); + let keyboard_layout = platform.keyboard_layout(); + let keyboard_mapper = platform.keyboard_mapper(); + + #[cfg(any(test, gpui_leak_detection))] + let _ref_counts = entities.ref_counts_drop_handle(); + + let app = Rc::new_cyclic(|this| AppCell { + app: RefCell::new(App { + this: this.clone(), + platform: platform.clone(), + text_system, + text_rendering_mode: Rc::new(Cell::new(TextRenderingMode::default())), + mode: GpuiMode::Production, + actions: Rc::new(ActionRegistry::default()), + flushing_effects: false, + pending_updates: 0, + active_drag: None, + platform_owned_drag: None, + background_executor, + foreground_executor, + #[cfg(feature = "profiler")] + foreground_journal, + #[cfg(all(feature = "profiler", not(target_family = "wasm")))] + hang_monitor: None, + svg_renderer: SvgRenderer::new(asset_source.clone()), + loading_assets: Default::default(), + asset_source, + http_client, + globals_by_type: Default::default(), + entities, + new_entity_observers: SubscriberSet::new(), + windows: SlotMap::with_key(), + window_update_stack: Vec::new(), + window_handles: FxHashMap::default(), + focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), + keymap: Rc::new(RefCell::new(Keymap::default())), + keyboard_layout, + keyboard_mapper, + global_action_listeners: Default::default(), + pending_effects: VecDeque::new(), + pending_notifications: FxHashSet::default(), + pending_global_notifications: Default::default(), + observers: SubscriberSet::new(), + tracked_entities: FxHashMap::default(), + window_invalidators_by_entity: FxHashMap::default(), + current_window_by_entity: FxHashMap::default(), + event_listeners: SubscriberSet::new(), + release_listeners: SubscriberSet::new(), + keystroke_observers: SubscriberSet::new(), + keystroke_interceptors: SubscriberSet::new(), + keyboard_layout_observers: SubscriberSet::new(), + missing_glyph_callback: Rc::default(), + thermal_state_observers: SubscriberSet::new(), + system_sleep_observers: SubscriberSet::new(), + system_wake_observers: SubscriberSet::new(), + global_observers: SubscriberSet::new(), + quit_observers: SubscriberSet::new(), + restart_observers: SubscriberSet::new(), + restart_path: None, + restart_arguments: Vec::new(), + window_closed_observers: SubscriberSet::new(), + layout_id_buffer: Default::default(), + propagate_event: true, + prompt_builder: Some(PromptBuilder::Default), + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_renderer: None, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_element_registry: InspectorElementRegistry::default(), + quit_mode: QuitMode::default(), + quitting: false, + cursor_hide_mode: CursorHideMode::default(), + reduce_motion: false, + synced_animation_epoch, + accessibility_force_disabled: false, + + #[cfg(any(test, feature = "test-support", debug_assertions))] + name: None, + element_arena: RefCell::new(Arena::new(1024 * 1024)), + event_arena: Arena::new(1024 * 1024), + + #[cfg(any(test, gpui_leak_detection))] + _ref_counts, + }), + }); + + init_app_menus(platform.as_ref(), &app.borrow()); + SystemWindowTabController::init(&mut app.borrow_mut()); + #[cfg(feature = "profiler")] + crate::profiler::journal::observe_power(&app.borrow()); + + platform.on_keyboard_layout_change(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.keyboard_layout = cx.platform.keyboard_layout(); + cx.keyboard_mapper = cx.platform.keyboard_mapper(); + cx.keyboard_layout_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + + platform.on_thermal_state_change(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.thermal_state_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + + platform.on_system_sleep(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.system_sleep_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + + platform.on_system_wake(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.system_wake_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + + platform.on_quit(Box::new({ + let cx = Rc::downgrade(&app); + move || { + let Some(cx) = cx.upgrade() else { + return true; + }; + match cx.try_borrow_mut() { + Ok(mut cx) => { + cx.shutdown(); + true + } + Err(_) => { + // Quit was requested while the AppCell was borrowed, so we can't shut down synchronously. + // The platform decides how to proceed. + false + } + } + } + })); + + app + } + + #[doc(hidden)] + pub fn ref_counts_drop_handle(&self) -> impl Sized + use<> { + self.entities.ref_counts_drop_handle() + } + + /// Captures a snapshot of all entities that currently have alive handles. + /// + /// The returned [`LeakDetectorSnapshot`] can later be passed to + /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no + /// entities created after the snapshot are still alive. + #[cfg(any(test, gpui_leak_detection))] + pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot { + self.entities.leak_detector_snapshot() + } + + /// Asserts that no entities created after `snapshot` still have alive handles. + /// + /// Entities that were already tracked at the time of the snapshot are ignored, + /// even if they still have handles. Only *new* entities (those whose + /// `EntityId` was not present in the snapshot) are considered leaks. + /// + /// # Panics + /// + /// Panics if any new entity handles exist. The panic message lists every + /// leaked entity with its type name, and includes allocation-site backtraces + /// when `LEAK_BACKTRACE` is set. + #[cfg(any(test, gpui_leak_detection))] + pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { + self.entities.assert_no_new_leaks(snapshot) + } + + /// Without leak detection compiled in, this records nothing. + #[cfg(all(feature = "test-support", not(any(test, gpui_leak_detection))))] + pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot { + LeakDetectorSnapshot::default() + } + + /// Without leak detection compiled in, this checks nothing. Set + /// `GPUI_LEAK_DETECTION` when building to enable it. + #[cfg(all(feature = "test-support", not(any(test, gpui_leak_detection))))] + pub fn assert_no_new_leaks(&self, _snapshot: &LeakDetectorSnapshot) {} + + /// Quit the application gracefully. + /// + /// Native applications give handlers registered with [`Context::on_app_quit`] + /// [`SHUTDOWN_TIMEOUT`] to complete. WebAssembly runs them asynchronously as best-effort cleanup + /// because its event-loop thread cannot block. + pub fn shutdown(&mut self) { + // Requested first so the final hang poll overlaps with the quit + // handlers. It's awaited alongside them, within `SHUTDOWN_TIMEOUT`. + #[cfg(all(feature = "profiler", not(target_family = "wasm")))] + let hang_monitor_flush = self + .hang_monitor + .as_ref() + .and_then(|hang_monitor| hang_monitor.request_flush()); + + let mut futures: Vec> = Vec::new(); + + for observer in self.quit_observers.remove(&()) { + futures.push(observer(self)); + } + + #[cfg(all(feature = "profiler", not(target_family = "wasm")))] + if let Some(flushed) = hang_monitor_flush { + futures.push( + async move { + if flushed.await.is_err() { + log::warn!("hang monitor exited before flushing"); + } + } + .boxed_local(), + ); + } + + self.windows.clear(); + self.window_handles.clear(); + self.flush_effects(); + self.quitting = true; + + let futures = futures::future::join_all(futures); + #[cfg(not(target_family = "wasm"))] + if self + .foreground_executor + .block_with_timeout(SHUTDOWN_TIMEOUT, futures) + .is_err() + { + log::error!("timed out waiting on app_will_quit"); + } + #[cfg(target_family = "wasm")] + self.foreground_executor.spawn(futures).detach(); + + self.quitting = false; + } + + /// Get the id of the current keyboard layout + pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout { + self.keyboard_layout.as_ref() + } + + /// Get the current keyboard mapper. + pub fn keyboard_mapper(&self) -> &Rc { + &self.keyboard_mapper + } + + /// Invokes a handler when the current keyboard layout changes + pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.keyboard_layout_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + + /// Gracefully quit the application via the platform's standard routine. + pub fn quit(&self) { + self.platform.quit(); + } + + /// Switches the platform between headless and windowed modes. + /// + /// On Linux, headless means no display server: windows opened afterwards lay out and handle + /// input but draw nothing. Windowed, the platform connects to the display server the + /// environment names. On macOS, the modes set the [`ActivationPolicy`]: headless is + /// `Accessory` (no Dock icon or menu bar) and windowed is `Regular`. Switching to windowed + /// doesn't activate the app: call [`App::activate`] for that. + /// + /// The returned task resolves once the switch has been applied. It fails if the platform is + /// already in the requested mode (switching to another display server means going headless + /// first), if the platform doesn't allow the mode or can't switch at all, if any window is + /// open (a window belongs to the display server that opened it), or if the display server + /// can't be reached. + pub fn request_windowing(&self, request: WindowingRequest) -> Task> { + if !self.windows.is_empty() { + return Task::ready(Err(anyhow::anyhow!( + "cannot switch windowing modes while windows are open" + ))); + } + self.platform.request_windowing(request) + } + + /// The environment of the display server the platform is connected to, or `None` while + /// headless. Its activation token is always unset, since the connection has used it. + /// + /// Programs an app launches inherit this process's environment, which may name another + /// graphical session, or none if the app started headless. Pass them this one with + /// [`GraphicalEnvironment::apply_to`](crate::GraphicalEnvironment::apply_to). + /// + /// On macOS, the environment carries nothing, and this is `None` while the activation + /// policy is `Accessory`. Always `None` on platforms that can't switch windowing modes. + pub fn graphical_environment(&self) -> Option { + self.platform.graphical_environment() + } + + /// Returns the current policy for hiding the cursor in response to + /// keyboard input. + pub fn cursor_hide_mode(&self) -> CursorHideMode { + self.cursor_hide_mode + } + + /// Sets the policy controlling when GPUI hides the cursor in response + /// to keyboard input. + pub fn set_cursor_hide_mode(&mut self, mode: CursorHideMode) { + self.cursor_hide_mode = mode; + } + + /// Returns whether the cursor is currently visible according to the + /// platform. This will report `false` after a keyboard input has hidden + /// the cursor and the user has not yet moved the mouse to restore it. + /// + /// See [`App::set_cursor_hide_mode`]. + pub fn is_cursor_visible(&self) -> bool { + self.platform.is_cursor_visible() + } + + /// Returns whether non-essential animations (e.g. loading spinners) should + /// be rendered in a static state instead of animating. + pub fn reduce_motion(&self) -> bool { + self.reduce_motion + } + + /// Sets whether non-essential animations (e.g. loading spinners) should be + /// rendered in a static state instead of animating. + pub fn set_reduce_motion(&mut self, reduce_motion: bool) { + if self.reduce_motion != reduce_motion { + self.reduce_motion = reduce_motion; + self.refresh_windows(); + } + } + + /// Schedules all windows in the application to be redrawn. This can be called + /// multiple times in an update cycle and still result in a single redraw. + pub fn refresh_windows(&mut self) { + self.pending_effects.push_back(Effect::RefreshWindows); + } + + #[inline(always)] + pub(crate) fn update(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { + self.start_update(); + let result = update(self); + self.finish_update(); + result + } + + pub(crate) fn start_update(&mut self) { + self.pending_updates += 1; + } + + #[inline(never)] + pub(crate) fn finish_update(&mut self) { + if !self.flushing_effects && self.pending_updates == 1 { + self.flushing_effects = true; + self.flush_effects(); + self.flushing_effects = false; + } + self.pending_updates -= 1; + } + + /// Arrange a callback to be invoked when the given entity calls `notify` on its respective context. + pub fn observe( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(Entity, &mut App) + 'static, + ) -> Subscription + where + W: 'static, + { + self.observe_internal(entity, move |e, cx| { + on_notify(e, cx); + true + }) + } + + pub(crate) fn detect_accessed_entities( + &mut self, + callback: impl FnOnce(&mut App) -> R, + ) -> (R, FxHashSet) { + let accessed_entities_start = self.entities.accessed_entities.get_mut().clone(); + let result = callback(self); + let entities_accessed_in_callback = self + .entities + .accessed_entities + .get_mut() + .difference(&accessed_entities_start) + .copied() + .collect::>(); + (result, entities_accessed_in_callback) + } + + pub(crate) fn record_entities_accessed( + &mut self, + window_handle: AnyWindowHandle, + invalidator: WindowInvalidator, + entities: &FxHashSet, + ) { + let mut tracked_entities = + std::mem::take(self.tracked_entities.entry(window_handle.id).or_default()); + for entity in tracked_entities.iter() { + self.window_invalidators_by_entity + .entry(*entity) + .and_modify(|windows| { + windows.remove(&window_handle.id); + }); + } + for entity in entities.iter() { + self.window_invalidators_by_entity + .entry(*entity) + .or_default() + .insert(window_handle.id, invalidator.clone()); + self.current_window_by_entity + .insert(*entity, window_handle.id); + } + tracked_entities.clear(); + tracked_entities.extend(entities.iter().copied()); + self.tracked_entities + .insert(window_handle.id, tracked_entities); + } + + pub(crate) fn new_observer(&mut self, key: EntityId, value: Handler) -> Subscription { + let (subscription, activate) = self.observers.insert(key, value); + self.defer(move |_| activate()); + subscription + } + + pub(crate) fn observe_internal( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(Entity, &mut App) -> bool + 'static, + ) -> Subscription + where + W: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + self.new_observer( + entity_id, + Box::new(move |cx| { + if let Some(entity) = handle.upgrade() { + on_notify(entity, cx) + } else { + false + } + }), + ) + } + + /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(Entity, &Event, &mut App) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Event: 'static, + { + self.subscribe_internal(entity, move |entity, event, cx| { + on_event(entity, event, cx); + true + }) + } + + pub(crate) fn new_subscription( + &mut self, + key: EntityId, + value: (TypeId, Listener), + ) -> Subscription { + let (subscription, activate) = self.event_listeners.insert(key, value); + self.defer(move |_| activate()); + subscription + } + pub(crate) fn subscribe_internal( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(Entity, &Evt, &mut App) -> bool + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + self.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + let event: &Evt = event.downcast_ref().expect("invalid event type"); + if let Some(entity) = handle.upgrade() { + on_event(entity, event, cx) + } else { + false + } + }), + ), + ) + } + + /// Returns handles to all open windows in the application. + /// Each handle could be downcast to a handle typed for the root view of that window. + /// To find all windows of a given type, you could filter on + pub fn windows(&self) -> Vec { + self.windows + .keys() + .flat_map(|window_id| self.window_handles.get(&window_id).copied()) + .collect() + } + + /// Returns the window handles ordered by their appearance on screen, front to back. + /// + /// The first window in the returned list is the active/topmost window of the application. + /// + /// This method returns None if the platform doesn't implement the method yet. + pub fn window_stack(&self) -> Option> { + self.platform.window_stack() + } + + /// Returns a handle to the window that is currently focused at the platform level, if one exists. + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + + /// Opens a new window with the given option and the root view returned by the given function. + /// The function is invoked with a `Window`, which can be used to interact with window-specific + /// functionality. + pub fn open_window( + &mut self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> anyhow::Result> { + self.update(|cx| { + let id = cx.windows.insert(None); + let handle = WindowHandle::new(id); + match Window::new(handle.into(), options, cx) { + Ok(mut window) => { + cx.window_update_stack.push(id); + let root_view = build_root_view(&mut window, cx); + cx.window_update_stack.pop(); + window.root.replace(root_view.into()); + window.defer(cx, |window: &mut Window, cx| window.appearance_changed(cx)); + + // allow a window to draw at least once before returning + // this didn't cause any issues on non windows platforms as it seems we always won the race to on_request_frame + // on windows we quite frequently lose the race and return a window that has never rendered, which leads to a crash + // where DispatchTree::root_node_id asserts on empty nodes + let clear = window.draw(cx); + clear.clear(cx); + + cx.window_handles.insert(id, window.handle); + cx.windows.get_mut(id).unwrap().replace(Box::new(window)); + Ok(handle) + } + Err(e) => { + cx.windows.remove(id); + Err(e) + } + } + }) + } + + /// Instructs the platform to activate the application by bringing it to the foreground. + pub fn activate(&self, ignoring_other_apps: bool) { + self.platform.activate(ignoring_other_apps); + } + + /// Hide the application at the platform level. + pub fn hide(&self) { + self.platform.hide(); + } + + /// Hide other applications at the platform level. + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + /// Unhide other applications at the platform level. + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + + /// Returns the list of currently active displays. + pub fn displays(&self) -> Vec> { + self.platform.displays() + } + + /// Returns the primary display that will be used for new windows. + pub fn primary_display(&self) -> Option> { + self.platform.primary_display() + } + + /// Returns whether `screen_capture_sources` may work. + pub fn is_screen_capture_supported(&self) -> bool { + self.platform.is_screen_capture_supported() + } + + /// Returns a list of available screen capture sources. + pub fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.platform.screen_capture_sources() + } + + /// Returns the display with the given ID, if one exists. + pub fn find_display(&self, id: DisplayId) -> Option> { + self.displays() + .iter() + .find(|display| display.id() == id) + .cloned() + } + + /// Returns the current thermal state of the system. + pub fn thermal_state(&self) -> ThermalState { + self.platform.thermal_state() + } + + /// Prevents idle sleep while the returned guard is held. + pub fn prevent_idle_sleep(&self, reason: &str) -> Task> { + self.platform.prevent_idle_sleep(reason) + } + + /// Invokes a handler when the thermal state changes + pub fn on_thermal_state_change(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.thermal_state_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + + /// Invokes a handler when the system is about to sleep. + /// + /// The platform gives the process only a short time before suspending, so + /// handlers should record state or cancel work rather than start it. + pub fn on_system_sleep(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.system_sleep_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + + /// Invokes a handler when the system wakes from sleep. + pub fn on_system_wake(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.system_wake_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + + /// Returns the appearance of the application's windows. + pub fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } + + /// Overrides the appearance (light/dark) applied to the app's windows, independent of + /// the OS-wide setting. Pass `None` to clear the override and follow the system again. + /// The current value is reported by [`App::window_appearance`]. + /// + /// On macOS this sets the underlying `NSApplication.appearance`, which controls the + /// native window chrome (the window border and titlebar) of every window. Use this + /// when the app uses a dark theme while the system is in light mode (or vice versa) + /// so the window edges render to match the theme. While an appearance is forced, + /// windows stop tracking system light/dark changes; pass `None` to resume following + /// the system. On other platforms this is a no-op. + pub fn set_window_appearance(&self, appearance: Option) { + self.platform.set_window_appearance(appearance); + } + + /// Returns the window button layout configuration when supported. + pub fn button_layout(&self) -> Option { + self.platform.button_layout() + } + + /// Reads data from the platform clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Reads data from the platform clipboard, resolving once the contents + /// are available. + /// + /// Prefer this over [`App::read_from_clipboard`] in code that can await: + /// on platforms where clipboard access is asynchronous and + /// permission-gated (e.g. web), the synchronous read always returns + /// `None` while this method performs a real read. + pub fn read_from_clipboard_async( + &self, + ) -> Task, ClipboardReadError>> { + self.platform.read_from_clipboard_async() + } + + /// Sets the text rendering mode for the application. + pub fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) { + self.text_rendering_mode.set(mode); + } + + /// Returns the current text rendering mode for the application. + pub fn text_rendering_mode(&self) -> TextRenderingMode { + self.text_rendering_mode.get() + } + + /// Writes data to the platform clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item) + } + + /// Reads data from the primary selection buffer. + /// Only available on Linux. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn read_from_primary(&self) -> Option { + self.platform.read_from_primary() + } + + /// Writes data to the primary selection buffer. + /// Only available on Linux. + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + pub fn write_to_primary(&self, item: ClipboardItem) { + self.platform.write_to_primary(item) + } + + /// Reads data from macOS's "Find" pasteboard. + /// + /// Used to share the current search string between apps. + /// + /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find + #[cfg(target_os = "macos")] + pub fn read_from_find_pasteboard(&self) -> Option { + self.platform.read_from_find_pasteboard() + } + + /// Writes data to macOS's "Find" pasteboard. + /// + /// Used to share the current search string between apps. + /// + /// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find + #[cfg(target_os = "macos")] + pub fn write_to_find_pasteboard(&self, item: ClipboardItem) { + self.platform.write_to_find_pasteboard(item) + } + + /// Writes credentials to the platform keychain. + pub fn write_credentials( + &self, + url: &str, + username: &str, + password: &[u8], + ) -> Task> { + self.platform.write_credentials(url, username, password) + } + + /// Reads credentials from the platform keychain. + pub fn read_credentials(&self, url: &str) -> Task)>>> { + self.platform.read_credentials(url) + } + + /// Deletes credentials from the platform keychain. + pub fn delete_credentials(&self, url: &str) -> Task> { + self.platform.delete_credentials(url) + } + + /// Directs the platform's default browser to open the given URL. + pub fn open_url(&self, url: &str) { + self.platform.open_url(url); + } + + /// Registers the given URL scheme (e.g. `zed` for `zed://` urls) to be + /// opened by the current app. + /// + /// On some platforms (e.g. macOS) you may be able to register URL schemes + /// as part of app distribution, but this method exists to let you register + /// schemes at runtime. + pub fn register_url_scheme(&self, scheme: &str) -> Task> { + self.platform.register_url_scheme(scheme) + } + + /// Sets the application's process-wide identity and user-visible name. + /// + /// The identifier is used for platform identity mechanisms such as the + /// Windows AppUserModelID. The name is used wherever the operating system + /// presents the application to the user. Call this once, early in startup, + /// before opening windows or posting notifications. + pub fn set_app_identity(&self, identifier: &str, name: &str) { + self.platform.set_app_identity(identifier, name); + } + + /// Posts a notification to the operating system's notification center. + /// + /// Posting a notification whose [`SystemNotification::tag`] matches an + /// earlier one replaces that notification where the platform supports it. + /// No-op on platforms without notification support, or when delivery is + /// unavailable (e.g. authorization was denied). + pub fn show_system_notification(&self, notification: SystemNotification) { + self.platform.show_system_notification(notification); + } + + /// Removes the delivered or pending notification with this tag. + /// + /// Best-effort: some platforms cannot retract a notification once shown, + /// in which case it ages out of the notification center on its own. + pub fn dismiss_system_notification(&self, tag: &str) { + self.platform.dismiss_system_notification(tag); + } + + /// Registers the handler invoked when the user activates a system + /// notification, either by clicking its body or one of its action + /// buttons. Subsequent registrations replace the handler. + pub fn on_system_notification_response(&self, mut callback: F) + where + F: 'static + FnMut(SystemNotificationResponse, &mut App), + { + let this = self.this.clone(); + self.platform + .on_system_notification_response(Box::new(move |response| { + if let Some(app) = this.upgrade() { + callback(response, &mut app.borrow_mut()); + } + })); + } + + /// Returns the full pathname of the current app bundle. + /// + /// Returns an error if the app is not being run from a bundle. + pub fn app_path(&self) -> Result { + self.platform.app_path() + } + + /// On Linux, returns the name of the compositor in use. + /// + /// Returns an empty string on other platforms. + pub fn compositor_name(&self) -> &'static str { + self.platform.compositor_name() + } + + /// Returns the file URL of the executable with the specified name in the application bundle + pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.platform.path_for_auxiliary_executable(name) + } + + /// Displays a platform modal for selecting paths. + /// + /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + /// May return an error on Linux if the file picker couldn't be opened. + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + self.platform.prompt_for_paths(options) + } + + /// Displays a platform modal for selecting a new path where a file can be saved. + /// + /// The provided directory will be used to set the initial location. + /// When a path is selected, it is relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. + /// May return an error on Linux if the file picker couldn't be opened. + pub fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + self.platform.prompt_for_new_path(directory, suggested_name) + } + + /// Reveals the specified path at the platform level, such as in Finder on macOS. + pub fn reveal_path(&self, path: &Path) { + self.platform.reveal_path(path) + } + + /// Opens the specified path with the system's default application. + pub fn open_with_system(&self, path: &Path) { + self.platform.open_with_system(path) + } + + /// Returns whether the user has configured scrollbars to auto-hide at the platform level. + pub fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } + + /// Restarts the application. + pub fn restart(&mut self) { + self.restart_observers + .clone() + .retain(&(), |observer| observer(self)); + self.platform.restart( + self.restart_path.take(), + std::mem::take(&mut self.restart_arguments), + ) + } + + /// Sets the path to use when restarting the application. + pub fn set_restart_path(&mut self, path: PathBuf) { + self.restart_path = Some(path); + } + + /// Returns the HTTP client for the application. + pub fn http_client(&self) -> Arc { + self.http_client.clone() + } + + /// Sets the HTTP client for the application. + pub fn set_http_client(&mut self, new_client: Arc) { + self.http_client = new_client; + } + + /// Configures when the application should automatically quit. + /// By default, [`QuitMode::Default`] is used. + pub fn set_quit_mode(&mut self, mode: QuitMode) { + self.quit_mode = mode; + } + + /// Sets whether the application participates in the system's foreground UI. + /// + /// Only has an effect on macOS, where [`Self::request_windowing`] normally sets it. Use this + /// for an accessory app that shows windows, such as a menu bar utility. It overrides the + /// policy until the next [`Self::request_windowing`], and the app counts as headless while + /// `Accessory`. After switching to [`ActivationPolicy::Regular`], activate the app yourself + /// with [`Self::activate`]; otherwise its menu bar may not appear until the app is + /// reactivated. + pub fn set_activation_policy(&mut self, policy: ActivationPolicy) { + self.platform.set_activation_policy(policy); + } + + /// Returns the SVG renderer used by the application. + pub fn svg_renderer(&self) -> SvgRenderer { + self.svg_renderer.clone() + } + + pub(crate) fn push_effect(&mut self, effect: Effect) { + match &effect { + Effect::Notify { emitter } => { + if !self.pending_notifications.insert(*emitter) { + return; + } + } + Effect::NotifyGlobalObservers { global_type } => { + if !self.pending_global_notifications.insert(*global_type) { + return; + } + } + _ => {} + }; + + self.pending_effects.push_back(effect); + } + + /// Called at the end of [`App::update`] to complete any side effects + /// such as notifying observers, emitting events, etc. Effects can themselves + /// cause effects, so we continue looping until all effects are processed. + fn flush_effects(&mut self) { + loop { + self.release_dropped_entities(); + self.release_dropped_focus_handles(); + if let Some(effect) = self.pending_effects.pop_front() { + match effect { + Effect::Notify { emitter } => { + self.apply_notify_effect(emitter); + } + + Effect::Emit { + emitter, + event_type, + event, + } => self.apply_emit_effect(emitter, event_type, &*event), + + Effect::RefreshWindows => { + self.apply_refresh_effect(); + } + + Effect::NotifyGlobalObservers { global_type } => { + self.apply_notify_global_observers_effect(global_type); + } + + Effect::Defer { callback } => { + self.apply_defer_effect(callback); + } + Effect::EntityCreated { + entity, + tid, + window, + } => { + self.apply_entity_created_effect(entity, tid, window); + } + } + } else { + #[cfg(any(test, feature = "test-support"))] + if matches!(self.mode, GpuiMode::Test { .. }) { + for window in self + .windows + .values() + .filter_map(|window| { + let window = window.as_deref()?; + window.invalidator.is_dirty().then_some(window.handle) + }) + .collect::>() + { + self.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + } + } + + if self.pending_effects.is_empty() { + for window in self.windows.values().filter_map(|window| window.as_deref()) { + if window.invalidator.is_dirty() + || window.needs_present.get() + || !window.next_frame_callbacks.borrow().is_empty() + { + window.platform_window.schedule_frame(); + } + } + + self.event_arena.clear(); + break; + } + } + } + } + + /// Repeatedly called during `flush_effects` to release any entities whose + /// reference count has become zero. We invoke any release observers before dropping + /// each entity. + fn release_dropped_entities(&mut self) { + loop { + let dropped = self.entities.take_dropped(); + if dropped.is_empty() { + break; + } + + for (entity_id, mut entity) in dropped { + self.observers.remove(&entity_id); + self.event_listeners.remove(&entity_id); + self.window_invalidators_by_entity.remove(&entity_id); + self.current_window_by_entity.remove(&entity_id); + for release_callback in self.release_listeners.remove(&entity_id) { + release_callback(entity.as_mut(), self); + } + } + } + } + + /// Repeatedly called during `flush_effects` to handle a focused handle being dropped. + fn release_dropped_focus_handles(&mut self) { + self.focus_handles + .clone() + .write() + .retain(|handle_id, focus| { + if focus.ref_count.load(SeqCst) == 0 { + for window_handle in self.windows() { + window_handle + .update(self, |_, window, cx| { + if window.focus == Some(handle_id) { + window.blur(cx); + } + }) + .unwrap(); + } + false + } else { + true + } + }); + } + + fn apply_notify_effect(&mut self, emitter: EntityId) { + self.pending_notifications.remove(&emitter); + + self.observers + .clone() + .retain(&emitter, |handler| handler(self)); + } + + fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: &dyn Any) { + self.event_listeners + .clone() + .retain(&emitter, |(stored_type, handler)| { + if *stored_type == event_type { + handler(event, self) + } else { + true + } + }); + } + + fn apply_refresh_effect(&mut self) { + for window in self.windows.values_mut() { + if let Some(window) = window.as_deref_mut() { + window.refreshing = true; + window.invalidator.set_dirty(true); + } + } + } + + fn apply_notify_global_observers_effect(&mut self, type_id: TypeId) { + self.pending_global_notifications.remove(&type_id); + self.global_observers + .clone() + .retain(&type_id, |observer| observer(self)); + } + + fn apply_defer_effect(&mut self, callback: Box) { + callback(self); + } + + fn apply_entity_created_effect( + &mut self, + entity: AnyEntity, + tid: TypeId, + window: Option, + ) { + // Seed the entity's current window from its creation context so + // `with_window` resolves correctly before the entity has ever been + // rendered. + if let Some(id) = window { + self.current_window_by_entity.insert(entity.entity_id(), id); + } + + self.new_entity_observers.clone().retain(&tid, |observer| { + if let Some(id) = window { + self.update_window_id(id, { + let entity = entity.clone(); + |_, window, cx| (observer)(entity, &mut Some(window), cx) + }) + .expect("All windows should be off the stack when flushing effects"); + } else { + (observer)(entity.clone(), &mut None, self) + } + true + }); + } + + /// Run `f` against the entity's *current* window — the most recently + /// rendered window that referenced the entity, or its creation window if + /// it has yet to be rendered. Returns `None` if the entity has no + /// current window, or if that window has been closed, or if it is + /// already on the update stack. + #[inline(always)] + pub fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let window_id = *self.current_window_by_entity.get(&entity_id)?; + self.update_window_id(window_id, |_, window, cx| f(window, cx)) + .ok() + } + + fn ensure_window(&mut self, entity_id: EntityId, window: WindowId) { + self.current_window_by_entity + .entry(entity_id) + .or_insert(window); + } + + #[inline(always)] + pub(crate) fn update_window_id(&mut self, id: WindowId, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut update = Some(update); + let mut result = None; + self.update_window_erased(id, &mut |arguments| { + if let Some((root_view, window, cx)) = arguments { + result = Some(update.take().unwrap()(root_view, window, cx)); + } else { + drop(update.take()); + drop(result.take()); + } + }); + result.context("window not found") + } + + /// Creates an `AsyncApp`, which can be cloned and has a static lifetime + /// so it can be held across `await` points. + pub fn to_async(&self) -> AsyncApp { + AsyncApp { + app: self.this.clone(), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Whether this app runs on the deterministic test scheduler. See + /// [`BackgroundExecutor::is_test`]. + pub fn is_test(&self) -> bool { + self.background_executor.is_test() + } + + /// Obtains a reference to the executor, which can be used to spawn futures. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + if self.quitting { + panic!("Can't spawn on main thread after on_app_quit") + }; + &self.foreground_executor + } + + /// Returns the foreground work journal for this app's foreground thread. + /// Apps constructed on the same thread share the stream. + #[cfg(feature = "profiler")] + pub fn foreground_journal(&self) -> crate::profiler::journal::ForegroundJournal { + self.foreground_journal.clone() + } + + /// Starts detecting foreground hangs on a dedicated thread. + /// + /// Nothing is spawned unless the app calls this. The thread polls a + /// detector over this app's foreground journal every `config.interval` + /// and passes each poll's incidents, including empty polls, to `on_poll` + /// on that thread. When the app quits, a final poll with + /// [`HangMonitorPollReason::Flush`] runs during shutdown, concurrently + /// with quit handlers and within [`SHUTDOWN_TIMEOUT`], so `on_poll` can + /// deliver batched results. + /// + /// # Errors + /// + /// Fails if the monitor was already started or its thread can't be + /// spawned. + /// + /// [`HangMonitorPollReason::Flush`]: crate::profiler::hang::HangMonitorPollReason::Flush + #[cfg(all(feature = "profiler", not(target_family = "wasm")))] + pub fn start_hang_monitor( + &mut self, + config: crate::profiler::hang::HangMonitorConfig, + on_poll: impl FnMut(crate::profiler::hang::HangMonitorPoll) + Send + 'static, + ) -> Result<(), crate::profiler::hang::HangMonitorError> { + use crate::profiler::hang::{HangDetector, HangMonitor, HangMonitorError}; + + if self.hang_monitor.is_some() { + debug_assert!(false, "the hang monitor was started twice"); + return Err(HangMonitorError::AlreadyStarted); + } + let detector = HangDetector::new( + self.foreground_journal(), + config.threshold, + config.frame_budget, + ); + let monitor = HangMonitor::spawn(detector, config.interval, on_poll) + .map_err(HangMonitorError::Spawn)?; + self.hang_monitor = Some(monitor); + Ok(()) + } + + /// Spawns the future returned by the given function on the main thread. The closure will be invoked + /// with [AsyncApp], which allows the application state to be accessed across await points. + #[track_caller] + #[inline(always)] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, + R: 'static, + { + let mut cx = self.prepare_spawn(); + + self.foreground_executor + .spawn(async move { f(&mut cx).await }.boxed_local()) + } + + /// Spawns the future returned by the given function on the main thread with + /// the given priority. The closure will be invoked with [AsyncApp], which + /// allows the application state to be accessed across await points. + pub fn spawn_with_priority(&self, priority: Priority, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, + R: 'static, + { + if self.quitting { + debug_panic!("Can't spawn on main thread after on_app_quit") + }; + + let mut cx = self.to_async(); + + self.foreground_executor + .spawn_with_priority(priority, async move { f(&mut cx).await }.boxed_local()) + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&mut self, f: impl FnOnce(&mut App) + 'static) { + self.push_effect(Effect::Defer { + callback: Box::new(f), + }); + } + + /// Accessor for the application's asset source, which is provided when constructing the `App`. + pub fn asset_source(&self) -> &Arc { + &self.asset_source + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Invokes a callback with grapheme clusters that exhausted font fallback. + /// + /// Registering a callback replaces the previous callback and enables missing-glyph + /// detection. The callback runs on the foreground executor after shaping has + /// released its internal locks. Dropping its subscription disables detection until + /// another callback is registered. + /// + /// Reports are queued without blocking shaping, then deduplicated before delivery. + /// The bounded queue can drop reports on overflow. Dropped reports may be reported + /// again when the text is reshaped; no retry is scheduled automatically. + pub fn on_missing_glyphs( + &self, + callback: impl FnMut(&[MissingGlyph], &mut App) + 'static, + ) -> Subscription { + let registration = self.missing_glyph_callback.replace(Box::new(callback)); + + if let Some(mut receiver) = self.text_system.take_missing_glyph_receiver() { + let callback = self.missing_glyph_callback.clone(); + self.spawn(async move |cx| { + while let Ok(missing_glyphs) = receiver.recv().await { + cx.update(|cx| callback.invoke(&missing_glyphs, cx)); + } + }) + .detach(); + } + self.text_system.enable_missing_glyph_reporting(); + + let callback = self.missing_glyph_callback.clone(); + let text_system = self.text_system.clone(); + Subscription::new(move || { + if callback.remove(®istration) { + text_system.disable_missing_glyph_reporting(); + } + }) + } + + /// Check whether a global of the given type has been assigned. + pub fn has_global(&self) -> bool { + self.globals_by_type.contains_key(&TypeId::of::()) + } + + /// Access the global of the given type. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global(&self) -> &G { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) + } + + /// Access the global of the given type if a value has been assigned. + pub fn try_global(&self) -> Option<&G> { + self.globals_by_type + .get(&TypeId::of::()) + .map(|any_state| any_state.downcast_ref::().unwrap()) + } + + /// Access the global of the given type mutably. Panics if a global for that type has not been assigned. + #[track_caller] + pub fn global_mut(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .get_mut(&global_type) + .and_then(|any_state| any_state.downcast_mut::()) + .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) + } + + /// Access the global of the given type mutably. A default value is assigned if a global of this type has not + /// yet been assigned. + pub fn default_global(&mut self) -> &mut G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type + .entry(global_type) + .or_insert_with(|| Box::::default()) + .downcast_mut::() + .unwrap() + } + + /// Sets the value of the global of the given type. + pub fn set_global(&mut self, global: G) { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, Box::new(global)); + } + + /// Clear all stored globals. Does not notify global observers. + #[cfg(any(test, feature = "test-support"))] + pub fn clear_globals(&mut self) { + self.globals_by_type.drain(); + } + + /// Remove the global of the given type from the app context. Does not notify global observers. + pub fn remove_global(&mut self) -> G { + let global_type = TypeId::of::(); + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + *self + .globals_by_type + .remove(&global_type) + .unwrap_or_else(|| panic!("no global added for {}", type_name::())) + .downcast() + .unwrap() + } + + /// Register a callback to be invoked when a global of the given type is updated. + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut Self) + 'static, + ) -> Subscription { + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + f(cx); + true + }), + ); + self.defer(move |_| activate()); + subscription + } + + /// Move the global of the given type to the stack. + #[track_caller] + pub(crate) fn lease_global(&mut self) -> GlobalLease { + GlobalLease::new( + self.globals_by_type + .remove(&TypeId::of::()) + .with_context(|| format!("no global registered of type {}", type_name::())) + .unwrap(), + ) + } + + /// Restore the global of the given type after it is moved to the stack. + pub(crate) fn end_global_lease(&mut self, lease: GlobalLease) { + let global_type = TypeId::of::(); + + self.push_effect(Effect::NotifyGlobalObservers { global_type }); + self.globals_by_type.insert(global_type, lease.global); + } + + pub(crate) fn new_entity_observer( + &self, + key: TypeId, + value: NewEntityListener, + ) -> Subscription { + let (subscription, activate) = self.new_entity_observers.insert(key, value); + activate(); + subscription + } + + /// Arrange for the given function to be invoked whenever a view of the specified type is created. + /// The function will be passed a mutable reference to the view along with an appropriate context. + pub fn observe_new( + &self, + on_new: impl 'static + Fn(&mut T, Option<&mut Window>, &mut Context), + ) -> Subscription { + self.new_entity_observer( + TypeId::of::(), + Box::new( + move |any_entity: AnyEntity, window: &mut Option<&mut Window>, cx: &mut App| { + any_entity + .downcast::() + .unwrap() + .update(cx, |entity_state, cx| { + on_new(entity_state, window.as_deref_mut(), cx) + }) + }, + ), + ) + } + + /// Observe the release of a entity. The callback is invoked after the entity + /// has no more strong references but before it has been dropped. + pub fn observe_release( + &self, + handle: &Entity, + on_release: impl FnOnce(&mut T, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let (subscription, activate) = self.release_listeners.insert( + handle.entity_id(), + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + on_release(entity, cx) + }), + ); + activate(); + subscription + } + + /// Observe the release of a entity. The callback is invoked after the entity + /// has no more strong references but before it has been dropped. + pub fn observe_release_in( + &self, + handle: &Entity, + window: &Window, + on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let window_handle = window.handle; + self.observe_release(handle, move |entity, cx| { + let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); + }) + } + + /// Register a callback to be invoked after a keystroke is resolved in any window, + /// including the action that handled it, if any. Keystrokes consumed by an + /// interceptor or raw keyboard event handler are not observed. + /// Standalone modifiers are observed on release. + pub fn observe_keystrokes( + &mut self, + mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + fn inner( + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_observers.insert((), handler); + activate(); + subscription + } + + inner( + &self.keystroke_observers, + Box::new(move |event, window, cx| { + f(event, window, cx); + true + }), + ) + } + + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires _before_ all other action and event mechanisms have resolved + /// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls + /// within interceptors will prevent action dispatch + pub fn intercept_keystrokes( + &mut self, + mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + fn inner( + keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_interceptors.insert((), handler); + activate(); + subscription + } + + inner( + &self.keystroke_interceptors, + Box::new(move |event, window, cx| { + f(event, window, cx); + true + }), + ) + } + + /// Register key bindings. + pub fn bind_keys(&mut self, bindings: impl IntoIterator) { + self.keymap.borrow_mut().add_bindings(bindings); + self.pending_effects.push_back(Effect::RefreshWindows); + } + + /// Clear all key bindings in the app. + pub fn clear_key_bindings(&mut self) { + self.keymap.borrow_mut().clear(); + self.pending_effects.push_back(Effect::RefreshWindows); + } + + /// Get all key bindings in the app. + pub fn key_bindings(&self) -> Rc> { + self.keymap.clone() + } + + /// Register a global handler for actions invoked via the keyboard. These handlers are run at + /// the end of the bubble phase for actions, and so will only be invoked if there are no other + /// handlers or if they called `cx.propagate()`. + pub fn on_action( + &mut self, + listener: impl Fn(&A, &mut Self) + 'static, + ) -> &mut Self { + self.global_action_listeners + .entry(TypeId::of::()) + .or_default() + .push(Rc::new(move |action, phase, cx| { + if phase == DispatchPhase::Bubble { + let action = action.downcast_ref().unwrap(); + listener(action, cx) + } + })); + self + } + + /// Event handlers propagate events by default. Call this method to stop dispatching to + /// event handlers with a lower z-index (mouse) or higher in the tree (keyboard). This is + /// the opposite of [`Self::propagate`]. It's also possible to cancel a call to [`Self::propagate`] by + /// calling this method before effects are flushed. + pub fn stop_propagation(&mut self) { + self.propagate_event = false; + } + + /// Action handlers stop propagation by default during the bubble phase of action dispatch + /// dispatching to action handlers higher in the element tree. This is the opposite of + /// [`Self::stop_propagation`]. It's also possible to cancel a call to [`Self::stop_propagation`] by calling + /// this method before effects are flushed. + pub fn propagate(&mut self) { + self.propagate_event = true; + } + + /// Build an action from some arbitrary data, typically a keymap entry. + pub fn build_action( + &self, + name: &str, + data: Option, + ) -> std::result::Result, ActionBuildError> { + self.actions.build_action(name, data) + } + + /// Get all action names that have been registered. Note that registration only allows for + /// actions to be built dynamically, and is unrelated to binding actions in the element tree. + pub fn all_action_names(&self) -> &[&'static str] { + self.actions.all_action_names() + } + + /// Returns key bindings that invoke the given action on the currently focused element, without + /// checking context. Bindings are returned in the order they were added. For display, the last + /// binding should take precedence. + pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { + RefCell::borrow(&self.keymap).all_bindings_for_input(input) + } + + /// Get all non-internal actions that have been registered, along with their schemas. + pub fn action_schemas( + &self, + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { + self.actions.action_schemas(generator) + } + + /// Get the schema for a specific action by name. + /// Returns `None` if the action is not found. + /// Returns `Some(None)` if the action exists but has no schema. + /// Returns `Some(Some(schema))` if the action exists and has a schema. + pub fn action_schema_by_name( + &self, + name: &str, + generator: &mut schemars::SchemaGenerator, + ) -> Option> { + self.actions.action_schema_by_name(name, generator) + } + + /// Get a map from a deprecated action name to the canonical name. + pub fn deprecated_actions_to_preferred_actions(&self) -> &HashMap<&'static str, &'static str> { + self.actions.deprecated_aliases() + } + + /// Get a map from an action name to the deprecation messages. + pub fn action_deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { + self.actions.deprecation_messages() + } + + /// Get a map from an action name to the documentation. + pub fn action_documentation(&self) -> &HashMap<&'static str, &'static str> { + self.actions.documentation() + } + + /// Register a callback to be invoked when the application is about to quit. + /// It is not possible to cancel the quit event at this point. + pub fn on_app_quit( + &self, + mut on_quit: impl FnMut(&mut App) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + let (subscription, activate) = self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + future.boxed_local() + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the application is about to restart. + /// + /// These callbacks are called before any `on_app_quit` callbacks. + pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription { + let (subscription, activate) = self.restart_observers.insert( + (), + Box::new(move |cx| { + on_restart(cx); + true + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when a window is closed + /// The window is no longer accessible at the point this callback is invoked. + pub fn on_window_closed( + &self, + mut on_closed: impl FnMut(&mut App, WindowId) + 'static, + ) -> Subscription { + let (subscription, activate) = self.window_closed_observers.insert((), Box::new(on_closed)); + activate(); + subscription + } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, window, cx| { + window.clear_pending_keystrokes(cx); + }) + .ok(); + } + } + + /// Checks if the given action is bound in the current context, as defined by the app's current focus, + /// the bindings in the element tree, and any global action listeners. + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + let mut action_available = false; + if let Some(window) = self.active_window() + && let Ok(window_action_available) = + window.update(self, |_, window, cx| window.is_action_available(action, cx)) + { + action_available = window_action_available; + } + + action_available + || self + .global_action_listeners + .contains_key(&action.as_any().type_id()) + } + + /// Sets the menu bar for this application. This will replace any existing menu bar. + pub fn set_menus(&self, menus: impl IntoIterator) { + let menus: Vec = menus.into_iter().collect(); + self.platform.set_menus(menus, &self.keymap.borrow()); + } + + /// Gets the menu bar for this application. + pub fn get_menus(&self) -> Option> { + self.platform.get_menus() + } + + /// Sets the right click menu for the app icon in the dock + pub fn set_dock_menu(&self, menus: Vec) { + self.platform.set_dock_menu(menus, &self.keymap.borrow()) + } + + /// Performs the action associated with the given dock menu item, only used on Windows for now. + pub fn perform_dock_menu_action(&self, action: usize) { + self.platform.perform_dock_menu_action(action); + } + + /// Adds given path to the bottom of the list of recent paths for the application. + /// The list is usually shown on the application icon's context menu in the dock, + /// and allows to open the recent files via that context menu. + /// If the path is already in the list, it will be moved to the bottom of the list. + pub fn add_recent_document(&self, path: &Path) { + self.platform.add_recent_document(path); + } + + /// Updates the jump list with the updated list of recent paths for the application, only used on Windows for now. + /// Note that this also sets the dock menu on Windows. + pub fn update_jump_list( + &self, + menus: Vec, + entries: Vec>, + ) -> Task>> { + self.platform.update_jump_list(menus, entries) + } + + /// Dispatch an action to the currently active window or global action handler + /// See [`crate::Action`] for more information on how actions work + pub fn dispatch_action(&mut self, action: &dyn Action) { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + .log_err(); + } else { + self.dispatch_global_action(action); + } + } + + fn dispatch_global_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event + && let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + + /// Is there currently something being dragged? + pub fn has_active_drag(&self) -> bool { + self.active_drag.is_some() + } + + /// Gets the cursor style of the currently active drag operation. + pub fn active_drag_cursor_style(&self) -> Option { + self.active_drag.as_ref().and_then(|drag| drag.cursor_style) + } + + /// Stops active drag and clears any related effects. + pub fn stop_active_drag(&mut self, window: &mut Window) -> bool { + if self.active_drag.is_some() { + self.active_drag = None; + if self.platform_owned_drag.as_ref().is_some_and(|drag| { + drag.source_window == window.window_handle().window_id() + && matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow) + }) { + self.platform_owned_drag = None; + } + window.refresh(); + true + } else { + false + } + } + + pub(crate) fn hand_active_drag_to_platform(&mut self, source_window: WindowId) -> bool { + let Some(drag) = self.active_drag.take() else { + return false; + }; + self.platform_owned_drag = Some(PlatformOwnedDrag { + source_window, + state: PlatformOwnedDragState::Suspended(drag), + }); + true + } + + pub(crate) fn restore_platform_drag(&mut self, source_window: WindowId) -> bool { + let Some(platform_drag) = self + .platform_owned_drag + .as_mut() + .filter(|drag| drag.source_window == source_window) + else { + return false; + }; + let state = std::mem::replace( + &mut platform_drag.state, + PlatformOwnedDragState::RestoredInSourceWindow, + ); + let PlatformOwnedDragState::Suspended(drag) = state else { + return false; + }; + self.active_drag = Some(drag); + true + } + + pub(crate) fn hand_restored_drag_to_platform(&mut self, source_window: WindowId) -> bool { + let Some(platform_drag) = self.platform_owned_drag.as_mut().filter(|drag| { + drag.source_window == source_window + && matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow) + }) else { + return false; + }; + let Some(drag) = self.active_drag.take() else { + return false; + }; + platform_drag.state = PlatformOwnedDragState::Suspended(drag); + true + } + + pub(crate) fn end_platform_drag(&mut self, source_window: WindowId) -> bool { + if !self + .platform_owned_drag + .as_ref() + .is_some_and(|drag| drag.source_window == source_window) + { + return false; + } + self.platform_owned_drag = None; + self.active_drag = None; + true + } + + /// Sets the cursor style for the currently active drag operation. + pub fn set_active_drag_cursor_style( + &mut self, + cursor_style: CursorStyle, + window: &mut Window, + ) -> bool { + if let Some(ref mut drag) = self.active_drag { + drag.cursor_style = Some(cursor_style); + window.refresh(); + true + } else { + false + } + } + + /// Set the prompt renderer for GPUI. This will replace the default or platform specific + /// prompts with this custom implementation. + pub fn set_prompt_builder( + &mut self, + renderer: impl Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle + + 'static, + ) { + self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer))); + } + + /// Reset the prompt builder to the default implementation. + pub fn reset_prompt_builder(&mut self) { + self.prompt_builder = Some(PromptBuilder::Default); + } + + /// Remove an asset from GPUI's cache + pub fn remove_asset(&mut self, source: &A::Source) { + let asset_id = (TypeId::of::(), hash(source)); + self.loading_assets.remove(&asset_id); + } + + /// Check whether an asset is present in GPUI's cache (loading or loaded), + /// without fetching it. + #[cfg(any(test, feature = "test-support"))] + pub fn has_asset(&self, source: &A::Source) -> bool { + let asset_id = (TypeId::of::(), hash(source)); + self.loading_assets.contains_key(&asset_id) + } + + /// Starts loading an uncached asset and returns its result once available. + /// + /// Pending loads and completed results are cached until [`Self::remove_asset`]. + /// This method does not subscribe a view to completion notifications. + pub fn fetch_asset(&mut self, source: &A::Source) -> Option { + self.asset_entry::(source).get() + } + + pub(crate) fn asset_entry(&mut self, source: &A::Source) -> &CachedLoad { + let asset_id = (TypeId::of::(), hash(source)); + if !self.loading_assets.contains_key(&asset_id) { + let future = A::load(source.clone(), self); + let entry = CachedLoad::new(future, self); + self.loading_assets.insert(asset_id, Box::new(entry)); + } + self.loading_assets + .get(&asset_id) + .and_then(|entry| entry.downcast_ref()) + .expect("asset cache entries are keyed by their asset type") + } + + /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus + /// for elements rendered within this window. + #[track_caller] + pub fn focus_handle(&self) -> FocusHandle { + FocusHandle::new(&self.focus_handles) + } + + /// Tell GPUI that an entity has changed and observers of it should be notified. + pub fn notify(&mut self, entity_id: EntityId) { + let window_invalidators = mem::take( + self.window_invalidators_by_entity + .entry(entity_id) + .or_default(), + ); + + // `window_invalidators_by_entity` is monotonic, so an entry alone + // doesn't mean the window is currently rendering the entity. Filter + // through `tracked_entities` to keep invalidation tight to windows + // that actually display this entity right now. + let live_invalidators: SmallVec<[WindowInvalidator; 2]> = window_invalidators + .iter() + .filter(|(window_id, _)| { + self.tracked_entities + .get(window_id) + .is_some_and(|set| set.contains(&entity_id)) + }) + .map(|(_, invalidator)| invalidator.clone()) + .collect(); + + if live_invalidators.is_empty() { + if self.pending_notifications.insert(entity_id) { + self.pending_effects + .push_back(Effect::Notify { emitter: entity_id }); + } + } else { + for invalidator in &live_invalidators { + invalidator.invalidate_view(entity_id, self); + } + } + + self.window_invalidators_by_entity + .insert(entity_id, window_invalidators); + } + + /// Returns the name for this [`App`]. + #[cfg(any(test, feature = "test-support", debug_assertions))] + pub fn get_name(&self) -> Option<&'static str> { + self.name + } + + /// Returns `true` if the platform file picker supports selecting a mix of files and directories. + pub fn can_select_mixed_files_and_dirs(&self) -> bool { + self.platform.can_select_mixed_files_and_dirs() + } + + /// Removes an image from the sprite atlas on all windows. + /// + /// If the current window is being updated, it will be removed from `App.windows`, you can use `current_window` to specify the current window. + /// This is a no-op if the image is not in the sprite atlas. + pub fn drop_image(&mut self, image: Arc, current_window: Option<&mut Window>) { + // remove the texture from all other windows + for window in self.windows.values_mut().flatten() { + _ = window.drop_image(image.clone()); + } + + // remove the texture from the current window + if let Some(window) = current_window { + _ = window.drop_image(image); + } + } + + /// Sets the renderer for the inspector. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn set_inspector_renderer(&mut self, f: crate::InspectorRenderer) { + self.inspector_renderer = Some(f); + } + + /// Registers a renderer specific to an inspector state. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn register_inspector_element( + &mut self, + factory: impl 'static + Fn(&mut Window, &mut App) -> F, + ) where + F: 'static + FnMut(crate::InspectorElementId, &T, &mut Window, &mut App) -> R, + { + self.inspector_element_registry.register(factory); + } + + /// Initializes gpui's default colors for the application. + /// + /// These colors can be accessed through `cx.default_colors()`. + pub fn init_colors(&mut self) { + self.set_global(GlobalColors(Arc::new(Colors::default()))); + } + + #[inline(never)] + fn update_window_erased( + &mut self, + window_id: WindowId, + update: &mut dyn FnMut(Option<(AnyView, &mut Window, &mut App)>), + ) { + self.update(|cx| { + let Some(mut window) = cx.windows.get_mut(window_id).and_then(Option::take) else { + update(None); + return; + }; + + let root_view = window.root.clone().unwrap(); + + cx.window_update_stack.push(window.handle.id); + update(Some((root_view, &mut window, cx))); + fn trail(window_id: WindowId, window: Box, cx: &mut App) -> Option<()> { + cx.window_update_stack.pop(); + + if window.removed { + cx.end_platform_drag(window_id); + cx.window_handles.remove(&window_id); + cx.windows.remove(window_id); + if let Some(tracked) = cx.tracked_entities.remove(&window_id) { + for entity_id in tracked { + if let Some(windows) = + cx.window_invalidators_by_entity.get_mut(&entity_id) + { + windows.remove(&window_id); + } + if cx.current_window_by_entity.get(&entity_id) == Some(&window_id) { + cx.current_window_by_entity.remove(&entity_id); + } + } + } + + cx.window_closed_observers.clone().retain(&(), |callback| { + callback(cx, window_id); + true + }); + + let quit_on_empty = match cx.quit_mode { + QuitMode::Explicit => false, + QuitMode::LastWindowClosed => true, + QuitMode::Default => cfg!(not(target_os = "macos")), + }; + + if quit_on_empty && cx.windows.is_empty() { + cx.quit(); + } + } else { + cx.windows.get_mut(window_id)?.replace(window); + } + Some(()) + } + if trail(window_id, window, cx).is_none() { + update(None); + } + }); + } + + #[inline(never)] + fn update_entity_erased( + &mut self, + handle: &AnyEntity, + entity_type: &str, + update: &mut dyn FnMut(&mut dyn Any, &mut App), + ) { + self.update(|cx| { + let mut lease = cx.entities.lease_erased(handle, entity_type); + update(lease.entity.as_deref_mut().unwrap(), cx); + cx.entities.end_lease_erased(handle.entity_id, lease); + }); + } + + #[inline(never)] + #[track_caller] + fn prepare_spawn(&self) -> AsyncApp { + if self.quitting { + debug_panic!("Can't spawn on main thread after on_app_quit") + }; + self.to_async() + } +} + +impl AppContext for App { + /// Builds an entity that is owned by the application. + /// + /// The given function will be invoked with a [`Context`] and must return an object representing the entity. An + /// [`Entity`] handle will be returned, which can be used to access the entity in a context. + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + self.update(|cx| { + let slot = cx.entities.reserve(); + let handle = slot.clone(); + let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); + + cx.push_effect(Effect::EntityCreated { + entity: handle.into_any(), + tid: TypeId::of::(), + window: cx.window_update_stack.last().cloned(), + }); + + cx.entities.insert(slot, entity) + }) + } + + fn reserve_entity(&mut self) -> Reservation { + Reservation(self.entities.reserve()) + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + self.update(|cx| { + let slot = reservation.0; + let entity = build_entity(&mut Context::new_context(cx, slot.downgrade())); + cx.entities.insert(slot, entity) + }) + } + + /// Updates the entity referenced by the given handle. The function is passed a mutable reference to the + /// entity along with a `Context` for the entity. + #[inline(always)] + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let mut update = Some(update); + let mut result = None; + self.update_entity_erased(handle, type_name::(), &mut |entity, cx| { + let value = update.take().unwrap()( + entity.downcast_mut::().unwrap(), + &mut Context::new_context(cx, handle.downgrade()), + ); + result = Some(value); + }); + result.unwrap() + } + + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static, + { + GpuiBorrow::new(handle.clone(), self) + } + + #[inline(always)] + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let entity = self.entities.read(handle); + read(entity, self) + } + + fn update_window(&mut self, handle: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.update_window_id(handle.id, update) + } + + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + App::with_window(self, entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let window = self + .windows + .get(window.id) + .context("window not found")? + .as_deref() + .expect("attempted to read a window that is already on the stack"); + + let root_view = window.root.clone().unwrap(); + let view = root_view + .downcast::() + .map_err(|_| anyhow!("root view's type has changed"))?; + + Ok(read(view, self)) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let mut g = self.global::(); + callback(g, self) + } +} + +/// These effects are processed at the end of each application update cycle. +pub(crate) enum Effect { + Notify { + emitter: EntityId, + }, + Emit { + emitter: EntityId, + event_type: TypeId, + event: ArenaBox, + }, + RefreshWindows, + NotifyGlobalObservers { + global_type: TypeId, + }, + Defer { + callback: Box, + }, + EntityCreated { + entity: AnyEntity, + tid: TypeId, + window: Option, + }, +} + +impl std::fmt::Debug for Effect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Effect::Notify { emitter } => write!(f, "Notify({})", emitter), + Effect::Emit { emitter, .. } => write!(f, "Emit({:?})", emitter), + Effect::RefreshWindows => write!(f, "RefreshWindows"), + Effect::NotifyGlobalObservers { global_type } => { + write!(f, "NotifyGlobalObservers({:?})", global_type) + } + Effect::Defer { .. } => write!(f, "Defer(..)"), + Effect::EntityCreated { entity, .. } => write!(f, "EntityCreated({:?})", entity), + } + } +} + +/// Wraps a global variable value during `update_global` while the value has been moved to the stack. +pub(crate) struct GlobalLease { + global: Box, + global_type: PhantomData, +} + +impl GlobalLease { + fn new(global: Box) -> Self { + GlobalLease { + global, + global_type: PhantomData, + } + } +} + +impl Deref for GlobalLease { + type Target = G; + + fn deref(&self) -> &Self::Target { + self.global.downcast_ref().unwrap() + } +} + +impl DerefMut for GlobalLease { + fn deref_mut(&mut self) -> &mut Self::Target { + self.global.downcast_mut().unwrap() + } +} + +/// Contains state associated with an active drag operation, started by dragging an element +/// within the window or by dragging into the app from the underlying platform. +pub struct AnyDrag { + /// The view used to render this drag + pub view: AnyView, + + /// The value of the dragged item, to be dropped + pub value: Arc, + + /// This is used to render the dragged item in the same place + /// on the original element that the drag was initiated + pub cursor_offset: Point, + + /// The cursor style to use while dragging + pub cursor_style: Option, + + /// Resolves the payload to offer the platform if the drag leaves the window. + /// Invoked at most once per drag gesture, at promotion time. + pub external_payload_source: Option, +} + +/// Lazily resolves the payload handed to the platform when an internal drag is +/// promoted to a native drag session. +pub type ExternalDragPayloadSource = + Box Option + 'static>; + +/// Contains state associated with a tooltip. You'll only need this struct if you're implementing +/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip](crate::Interactivity::tooltip). +#[derive(Clone)] +pub struct AnyTooltip { + /// The view used to display the tooltip + pub view: AnyView, + + /// The absolute position of the mouse when the tooltip was deployed. + pub mouse_position: Point, + + /// Given the bounds of the tooltip, checks whether the tooltip should still be visible and + /// updates its state accordingly. This is needed atop the hovered element's mouse move handler + /// to handle the case where the element is not painted (e.g. via use of `visible_on_hover`). + pub check_visible_and_update: Rc, &mut Window, &mut App) -> bool>, +} + +/// Whether a keystroke should prefer character input or key bindings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InputPreference { + /// Prefer typing text over triggering key bindings. + CharacterInput, + /// Dispatch key bindings normally, if any match. + KeyBindings, +} + +/// A keystroke event, and potentially the associated action +#[derive(Debug)] +pub struct KeystrokeEvent { + /// The keystroke that occurred + pub keystroke: Keystroke, + + /// Whether this keystroke should prefer character input or key bindings. + /// This is [`InputPreference::CharacterInput`] when the platform prefers text for the key + /// (e.g. AltGr on Windows) and the focused input accepts text. Interceptors still receive + /// these keystrokes and can consume them. + /// + /// If the keystroke is part of a multi-stroke binding, it still waits as pending input + /// even when this is [`InputPreference::CharacterInput`]. + pub input_preference: InputPreference, + + /// The action that was resolved for the keystroke, if any + pub action: Option>, + + /// The context stack at the time + pub context_stack: Vec, +} + +struct NullHttpClient; + +impl HttpClient for NullHttpClient { + fn send( + &self, + _req: http_client::Request, + ) -> futures::future::BoxFuture< + 'static, + anyhow::Result>, + > { + async move { + anyhow::bail!("No HttpClient available"); + } + .boxed() + } + + fn user_agent(&self) -> Option<&http_client::http::HeaderValue> { + None + } + + fn proxy(&self) -> Option<&Url> { + None + } +} + +/// A mutable reference to an entity owned by GPUI +pub struct GpuiBorrow<'a, T> { + inner: Option>, + app: &'a mut App, +} + +impl<'a, T: 'static> GpuiBorrow<'a, T> { + fn new(inner: Entity, app: &'a mut App) -> Self { + app.start_update(); + let lease = app.entities.lease(&inner); + Self { + inner: Some(lease), + app, + } + } +} + +impl<'a, T: 'static> std::borrow::Borrow for GpuiBorrow<'a, T> { + fn borrow(&self) -> &T { + self.inner.as_ref().unwrap().borrow() + } +} + +impl<'a, T: 'static> std::borrow::BorrowMut for GpuiBorrow<'a, T> { + fn borrow_mut(&mut self) -> &mut T { + self.inner.as_mut().unwrap().borrow_mut() + } +} + +impl<'a, T: 'static> std::ops::Deref for GpuiBorrow<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.as_ref().unwrap() + } +} + +impl<'a, T: 'static> std::ops::DerefMut for GpuiBorrow<'a, T> { + fn deref_mut(&mut self) -> &mut T { + self.inner.as_mut().unwrap() + } +} + +impl<'a, T> Drop for GpuiBorrow<'a, T> { + fn drop(&mut self) { + let lease = self.inner.take().unwrap(); + self.app.notify(lease.id); + self.app.entities.end_lease(lease); + self.app.finish_update(); + } +} + +#[cfg(test)] +mod test { + use std::{ + cell::{Cell, RefCell}, + ffi::OsString, + path::PathBuf, + rc::Rc, + }; + + #[cfg(unix)] + use std::os::unix::ffi::OsStringExt; + + use crate::{ + AppContext, Context, Empty, FallbackFontClass, IntoElement, MissingGlyph, Render, + TestAppContext, Window, + }; + + struct RenderCounter(Rc>); + + impl Render for RenderCounter { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.0.set(self.0.get() + 1); + Empty + } + } + + #[gpui::test] + fn async_app_refresh_flushes_refresh_effect(cx: &mut TestAppContext) { + let render_count = Rc::new(Cell::new(0)); + + let _window = cx.add_window({ + let render_count = render_count.clone(); + move |_, _| RenderCounter(render_count) + }); + + cx.run_until_parked(); + let render_count_before_refresh = render_count.get(); + + cx.to_async().refresh(); + + assert_eq!(render_count.get(), render_count_before_refresh + 1); + } + + #[gpui::test] + fn missing_glyph_callbacks_follow_subscription_lifetime(cx: &mut TestAppContext) { + let observed = Rc::new(RefCell::new(Vec::new())); + let subscription = cx.update(|cx| { + let observed = observed.clone(); + cx.on_missing_glyphs(move |missing_glyphs, _| { + observed.borrow_mut().extend_from_slice(missing_glyphs); + }) + }); + cx.update(|cx| { + cx.text_system() + .report_missing_glyphs_in_test(vec![missing_glyph("active")]); + }); + cx.run_until_parked(); + assert_eq!(observed.borrow().as_slice(), &[missing_glyph("active")]); + + let second_observed = Rc::new(RefCell::new(Vec::new())); + let second_subscription = cx.update(|cx| { + let second_observed = second_observed.clone(); + cx.on_missing_glyphs(move |missing_glyphs, _| { + second_observed + .borrow_mut() + .extend_from_slice(missing_glyphs); + }) + }); + cx.update(|cx| { + cx.text_system() + .report_missing_glyphs_in_test(vec![missing_glyph("replacement")]); + }); + cx.run_until_parked(); + assert_eq!(observed.borrow().as_slice(), &[missing_glyph("active")]); + assert_eq!( + second_observed.borrow().as_slice(), + &[missing_glyph("replacement")] + ); + + drop(subscription); + cx.update(|cx| { + cx.text_system() + .report_missing_glyphs_in_test(vec![missing_glyph("after old drop")]); + }); + cx.run_until_parked(); + assert_eq!( + second_observed.borrow().as_slice(), + &[ + missing_glyph("replacement"), + missing_glyph("after old drop") + ] + ); + + drop(second_subscription); + cx.update(|cx| { + cx.text_system() + .report_missing_glyphs_in_test(vec![missing_glyph("inactive")]); + }); + cx.run_until_parked(); + assert_eq!(second_observed.borrow().len(), 2); + } + + #[test] + fn test_gpui_borrow() { + let cx = TestAppContext::single(); + let observation_count = Rc::new(RefCell::new(0)); + + let state = cx.update(|cx| { + let state = cx.new(|_| false); + cx.observe(&state, { + let observation_count = observation_count.clone(); + move |_, _| { + let mut count = observation_count.borrow_mut(); + *count += 1; + } + }) + .detach(); + + state + }); + + cx.update(|cx| { + // Calling this like this so that we don't clobber the borrow_mut above + *std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true; + }); + + cx.update(|cx| { + state.write(cx, false); + }); + + assert_eq!(*observation_count.borrow(), 2); + } + + #[gpui::test] + async fn test_restart_preserves_path_and_arguments(cx: &mut TestAppContext) { + #[cfg(unix)] + let user_data_dir = OsString::from_vec(b"/tmp/zed data/\xff".to_vec()); + #[cfg(not(unix))] + let user_data_dir = OsString::from("C:\\zed data"); + let arguments = vec![OsString::from("--user-data-dir"), user_data_dir]; + let restart_path = PathBuf::from("updated-zed"); + let _application = + super::Application(cx.app.clone()).with_restart_arguments(arguments.clone()); + let restart = cx.expect_restart(); + + cx.update(|cx| { + cx.set_restart_path(restart_path.clone()); + cx.restart(); + }); + + let (path, restart_arguments) = restart.await.expect("restart was not requested"); + assert_eq!(path, Some(restart_path)); + assert_eq!(restart_arguments, arguments); + } + + fn missing_glyph(grapheme: &'static str) -> MissingGlyph { + MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional) + } +} diff --git a/vendor/gpui-pre/src/app/async_context.rs b/vendor/gpui-pre/src/app/async_context.rs new file mode 100644 index 0000000..7190dff --- /dev/null +++ b/vendor/gpui-pre/src/app/async_context.rs @@ -0,0 +1,557 @@ +use crate::{ + AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, + Entity, EntityId, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow, + PromptButton, PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, + Window, WindowHandle, +}; +use anyhow::{Context as _, bail}; +use derive_more::{Deref, DerefMut}; +use futures::channel::oneshot; +use futures::future::FutureExt; +use std::{future::Future, rc::Weak}; + +use super::{Context, WeakEntity}; + +/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code. +/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async]. +/// +/// Internally, this holds a weak reference to an `App`. Methods will panic if the app has been dropped, +/// but this should not happen in practice when using foreground tasks spawned via `cx.spawn()`, +/// as the executor checks if the app is alive before running each task. +#[derive(Clone)] +pub struct AsyncApp { + pub(crate) app: Weak, + pub(crate) background_executor: BackgroundExecutor, + pub(crate) foreground_executor: ForegroundExecutor, +} + +impl AsyncApp { + fn app(&self) -> std::rc::Rc { + self.app + .upgrade() + .expect("app was released before async operation completed") + } +} + +impl AppContext for AsyncApp { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let app = self.app(); + let mut app = app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> Reservation { + let app = self.app(); + let mut app = app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let app = self.app(); + let mut app = app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + #[inline(always)] + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let app = self.app(); + let mut app = app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _handle: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static, + { + panic!("Cannot as_mut with an async context. Try calling update() first") + } + + #[inline(always)] + fn read_entity(&self, handle: &Entity, callback: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let app = self.app(); + let lock = app.borrow(); + lock.read_entity(handle, callback) + } + + #[inline(always)] + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let app = self.app.upgrade().context("app was released")?; + let mut lock = app.try_borrow_mut()?; + if lock.quitting { + bail!("app is quitting"); + } + lock.update_window(window, f) + } + + #[inline(always)] + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let app = self.app.upgrade()?; + let mut lock = app.try_borrow_mut().ok()?; + if lock.quitting { + return None; + } + lock.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.upgrade().context("app was released")?; + let lock = app.borrow(); + if lock.quitting { + bail!("app is quitting"); + } + lock.read_window(window, read) + } + + #[track_caller] + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let app = self.app(); + let mut lock = app.borrow_mut(); + lock.update(|this| this.read_global(callback)) + } +} + +impl AsyncApp { + /// Schedules all windows in the application to be redrawn. + pub fn refresh(&self) { + let app = self.app(); + let mut lock = app.borrow_mut(); + // A direct call would leave the refresh effect queued, which cannot wake + // a platform render loop that has already parked. + lock.update(|cx| cx.refresh_windows()); + } + + /// Get an executor which can be used to spawn futures in the background. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Whether this app runs on the deterministic test scheduler. See + /// [`BackgroundExecutor::is_test`]. + pub fn is_test(&self) -> bool { + self.background_executor.is_test() + } + + /// Get an executor which can be used to spawn futures in the foreground. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Invoke the given function in the context of the app, then flush any effects produced during its invocation. + #[inline(always)] + pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { + let app = self.app(); + let mut lock = app.borrow_mut(); + lock.update(f) + } + + /// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( + &mut self, + entity: &Entity, + on_event: impl FnMut(Entity, &Event, &mut App) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Event: 'static, + { + let app = self.app(); + let mut lock = app.borrow_mut(); + lock.subscribe(entity, on_event) + } + + /// Open a window with the given options based on the root view returned by the given function. + pub fn open_window( + &self, + options: crate::WindowOptions, + build_root_view: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> Result> + where + V: 'static + Render, + { + let app = self.app(); + let mut lock = app.borrow_mut(); + if lock.quitting { + bail!("app is quitting"); + } + lock.open_window(options, build_root_view) + } + + /// Schedule a future to be polled in the foreground. + #[track_caller] + #[inline(always)] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncApp) -> R + 'static, + R: 'static, + { + let mut cx = self.clone(); + self.foreground_executor + .spawn(async move { f(&mut cx).await }.boxed_local()) + } + + /// Determine whether global state of the specified type has been assigned. + pub fn has_global(&self) -> bool { + let app = self.app(); + let app = app.borrow_mut(); + app.has_global::() + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Panics if no global state of the specified type has been assigned. + #[inline(always)] + pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> R { + let app = self.app(); + let app = app.borrow_mut(); + read(app.global(), &app) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// + /// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking + pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { + let app = self.app(); + let app = app.borrow_mut(); + if app.quitting { + return None; + } + Some(read(app.try_global()?, &app)) + } + + /// Reads the global state of the specified type, passing it to the given callback. + /// A default value is assigned if a global of this type has not yet been assigned. + pub fn read_default_global( + &self, + read: impl FnOnce(&G, &App) -> R, + ) -> R { + let app = self.app(); + let mut app = app.borrow_mut(); + app.update(|cx| { + cx.default_global::(); + }); + read(app.global(), &app) + } + + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global) + /// for updating the global state of the specified type. + pub fn update_global(&self, update: impl FnOnce(&mut G, &mut App) -> R) -> R { + let app = self.app(); + let mut app = app.borrow_mut(); + app.update(|cx| cx.update_global(update)) + } + + /// Run something using this entity and cx, when the returned struct is dropped + pub fn on_drop) + 'static>( + &self, + entity: &WeakEntity, + f: Callback, + ) -> gpui_util::Deferred> { + let entity = entity.clone(); + let mut cx = self.clone(); + gpui_util::defer(move || { + entity.update(&mut cx, f).ok(); + }) + } +} + +/// A cloneable, owned handle to the application context, +/// composed with the window associated with the current task. +#[derive(Clone, Deref, DerefMut)] +pub struct AsyncWindowContext { + #[deref] + #[deref_mut] + app: AsyncApp, + window: AnyWindowHandle, +} + +impl AsyncWindowContext { + pub(crate) fn new_context(app: AsyncApp, window: AnyWindowHandle) -> Self { + Self { app, window } + } + + /// Get the handle of the window this context is associated with. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + /// A convenience method for [`App::update_window`]. + #[inline(always)] + pub fn update(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> Result { + self.app + .update_window(self.window, |_, window, cx| update(window, cx)) + } + + /// A convenience method for [`App::update_window`]. + #[inline(always)] + pub fn update_root( + &mut self, + update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, + ) -> Result { + self.app.update_window(self.window, update) + } + + /// A convenience method for [`Window::on_next_frame`]. + pub fn on_next_frame(&mut self, f: impl FnOnce(&mut Window, &mut App) + 'static) { + self.app + .update_window(self.window, |_, window, _| window.on_next_frame(f)) + .ok(); + } + + /// A convenience method for [`App::global`]. + pub fn read_global( + &mut self, + read: impl FnOnce(&G, &Window, &App) -> R, + ) -> Result { + self.app + .update_window(self.window, |_, window, cx| read(cx.global(), window, cx)) + } + + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global). + /// for updating the global state of the specified type. + pub fn update_global( + &mut self, + update: impl FnOnce(&mut G, &mut Window, &mut App) -> R, + ) -> Result + where + G: Global, + { + self.app.update_window(self.window, |_, window, cx| { + cx.update_global(|global, cx| update(global, window, cx)) + }) + } + + /// Schedule a future to be executed on the main thread. This is used for collecting + /// the results of background tasks and updating the UI. + #[track_caller] + #[inline(always)] + pub fn spawn(&self, f: AsyncFn) -> Task + where + AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, + R: 'static, + { + let mut cx = self.clone(); + self.foreground_executor + .spawn(async move { f(&mut cx).await }.boxed_local()) + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[T], + ) -> oneshot::Receiver + where + T: Clone + Into, + { + self.app + .update_window(self.window, |_, window, cx| { + window.prompt(level, message, detail, answers, cx) + }) + .unwrap_or_else(|_| oneshot::channel().1) + } +} + +impl AppContext for AsyncWindowContext { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity + where + T: 'static, + { + let mut build_entity = Some(build_entity); + match self.app.update_window(self.window, |_, _, cx| { + cx.new( + build_entity + .take() + .expect("build_entity is taken exactly once"), + ) + }) { + Ok(entity) => entity, + Err(_) => self.app.new( + build_entity + .take() + .expect("update_window returned Err without invoking the closure"), + ), + } + } + + fn reserve_entity(&mut self) -> Reservation { + self.app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let mut args = Some((reservation, build_entity)); + match self.app.update_window(self.window, |_, _, cx| { + let (reservation, build_entity) = args.take().expect("args are taken exactly once"); + cx.insert_entity(reservation, build_entity) + }) { + Ok(entity) => entity, + Err(_) => { + let (reservation, build_entity) = args + .take() + .expect("update_window returned Err without invoking the closure"); + self.app.insert_entity(reservation, build_entity) + } + } + } + + #[inline(always)] + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + self.app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static, + { + panic!("Cannot use as_mut() from an async context, call `update`") + } + + #[inline(always)] + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + self.app.read_entity(handle, read) + } + + #[inline(always)] + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.app.update_window(window, update) + } + + #[inline(always)] + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + self.app.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + self.app.read_window(window, read) + } + + #[track_caller] + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.app.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + self.app.read_global(callback) + } +} + +impl VisualContext for AsyncWindowContext { + type Result = Result; + + fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Result> { + self.app.update_window(self.window, |_, window, cx| { + cx.new(|cx| build_entity(window, cx)) + }) + } + + fn update_window_entity( + &mut self, + view: &Entity, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Result { + let view = view.clone(); + self.app + .with_window(view.entity_id(), |window, app| { + view.update(app, |entity, cx| update(entity, window, cx)) + }) + .context("entity has no current window") + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Result> + where + V: 'static + Render, + { + self.app.update_window(self.window, |_, window, cx| { + window.replace_root(cx, build_view) + }) + } + + fn focus(&mut self, view: &Entity) -> Result<()> + where + V: Focusable, + { + self.app.update_window(self.window, |_, window, cx| { + view.read(cx).focus_handle(cx).focus(window, cx); + }) + } +} diff --git a/vendor/gpui-pre/src/app/bench_context.rs b/vendor/gpui-pre/src/app/bench_context.rs new file mode 100644 index 0000000..2cf504c --- /dev/null +++ b/vendor/gpui-pre/src/app/bench_context.rs @@ -0,0 +1,1596 @@ +use std::{ + cell::{OnceCell, RefCell}, + future::Future, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::{Duration, Instant}, +}; + +use anyhow::{Result, anyhow}; +pub use bench_metrics::{BenchMeasurement, CountingAllocator, MetricReport}; +use hdrhistogram::Histogram; + +use crate::{ + AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, Bounds, Context, Empty, + Entity, EntityId, Focusable, ForegroundExecutor, Global, Platform, PlatformHeadlessRenderer, + PlatformTextSystem, Render, Reservation, Task, TestPlatform, ThreadedDispatcher, VisualContext, + Window, WindowBounds, WindowHandle, WindowOptions, + app::GpuiBorrow, + profiler::{ + self, FrameEvent, FrameTimingCollector, + journal::{ForegroundEvent, ForegroundJournalCollector, ForegroundJournalEntry}, + }, +}; + +/// Returns a benchmark platform backed by this thread's shared dispatcher. +/// +/// The platform uses this thread's shared multithreaded [`ThreadedDispatcher`], so +/// background work runs with production concurrency in real time. The dispatcher +/// is cached per thread and reused across benchmark invocations so worker and +/// timer threads persist for the whole process instead of being recreated for +/// every Criterion calibration pass. +/// +/// Text is shaped with the provided platform text system. Benchmarks generated +/// by `#[gpui::bench]` use the current platform's text system, so text-heavy +/// benchmark measurements include production shaping and glyph rasterization. +/// +/// `headless_renderer_factory` supplies a renderer for benchmark windows, e.g. +/// `gpui_platform::current_headless_renderer`. When present, scenes drawn by +/// benchmarks are rasterized through the real sprite atlas and submitted to +/// the GPU on present, so quad/sprite regressions show up in measurements. +/// When `None`, presenting discards the scene. Currently only macOS provides +/// a headless renderer (Metal), so GPU submission is excluded from benchmark +/// measurements on other platforms. +pub fn bench_platform( + headless_renderer_factory: Option< + Box anyhow::Result>>>, + >, + text_system: Arc, +) -> Rc { + thread_local! { + static DISPATCHER: OnceCell> = const { OnceCell::new() }; + } + let dispatcher = DISPATCHER.with(|cell| { + cell.get_or_init(|| Arc::new(ThreadedDispatcher::new())) + .clone() + }); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + TestPlatform::with_platform( + background_executor, + foreground_executor, + text_system, + headless_renderer_factory, + ) as Rc +} + +/// Default target frame rate when a benchmark doesn't specify `fps = N`. +const DEFAULT_FPS: u64 = 120; + +const NANOS_PER_SECOND: u128 = 1_000_000_000; + +/// Aggregate statistics for total foreground executor work observed during a +/// measured interval, returned by [`BenchReport::foreground_work`]. +#[derive(Clone, Copy, Debug)] +pub struct ForegroundWorkSummary { + /// Number of foreground work items recorded: task polls, action + /// handlers, input dispatches, and folded sub-floor poll flushes. + pub count: u64, + /// Sum of every recorded item's duration. + pub total: Duration, + /// The longest single recorded item. + pub max: Duration, + /// 50th percentile duration. + pub p50: Duration, + /// 90th percentile duration. + pub p90: Duration, + /// 95th percentile duration. + pub p95: Duration, + /// 99th percentile duration. + pub p99: Duration, + /// How many whole frame budgets (at the report's configured FPS) were + /// exceeded in total, summed across every recorded item. + pub frame_budget_overruns_total: u64, + /// How many whole frame budgets the longest recorded item exceeded. + pub frame_budget_overruns_max: u64, +} + +/// A small report produced by GPUI benchmarks. +#[derive(Clone)] +pub struct BenchReport { + frame_snapshot: Rc>, + metrics: MetricReport, + frame_budget_nanos: u128, +} + +impl Default for BenchReport { + fn default() -> Self { + Self::with_fps(DEFAULT_FPS) + } +} + +impl BenchReport { + /// Creates a report whose per-frame budget is one frame at `fps` when + /// counting frame budget overruns. + pub fn with_fps(fps: u64) -> Self { + assert!(fps > 0, "frame rate must be greater than zero"); + Self::with_frame_budget_nanos(NANOS_PER_SECOND / fps as u128) + } + + /// Creates a report that treats `frame_budget_nanos` as the per-frame budget + /// when counting frame budget overruns. + pub fn with_frame_budget_nanos(frame_budget_nanos: u128) -> Self { + assert!( + frame_budget_nanos > 0, + "frame budget must be at least one nanosecond" + ); + Self { + frame_snapshot: Rc::new(RefCell::new(WindowFrameSnapshot::new())), + metrics: MetricReport::new(), + frame_budget_nanos, + } + } + + /// Returns the per-iteration secondary metrics (retired instructions, + /// cycles, context switches, ...) recorded alongside Criterion's primary + /// measurement. + pub fn metrics(&self) -> &MetricReport { + &self.metrics + } + + fn record_frame_timings<'i>(&self, events: impl IntoIterator) { + let mut snapshot = self.frame_snapshot.borrow_mut(); + // `.ok()` on `record`: this operation is infallible (the histograms auto-resize). + for event in events { + match event { + FrameEvent::Draw(timing) => { + snapshot + .draw + .record(timing.draw_duration().as_nanos() as u64) + .ok(); + if let Some(dirty_to_draw) = timing.dirty_to_draw_duration() { + snapshot + .dirty_to_draw + .record(dirty_to_draw.as_nanos() as u64) + .ok(); + } + if timing.invalidations > 0 { + snapshot + .invalidations_per_frame + .record(timing.invalidations) + .ok(); + } + } + FrameEvent::Present(timing) => { + if let Some(animation_interval) = timing.animation_interval { + snapshot + .present_interval + .record(animation_interval.as_nanos() as u64) + .ok(); + } + } + } + } + } + + /// Records total foreground executor work observed during a measured + /// interval: task polls, action handlers, and input dispatches, whether + /// or not they produced a window draw. Draws and presents are excluded + /// here since [`Self::record_frame_timings`] already accounts for them. + fn record_foreground_events<'i>(&self, events: impl IntoIterator) { + let mut snapshot = self.frame_snapshot.borrow_mut(); + for event in events { + let duration = match event { + ForegroundEvent::Draw(_) | ForegroundEvent::Present(_) => continue, + // A flush's span (used by `ForegroundEvent::duration`) is not + // the time spent polling; its summary total is. + ForegroundEvent::SmallPolls(flush) => flush.summary.total, + _ => event.duration(), + }; + snapshot.foreground_work.record(duration); + } + } + + fn total_budget_overruns(&self, histogram: &Histogram) -> u64 { + histogram + .iter_recorded() + .map(|value| { + self.budget_overruns(Duration::from_nanos(value.value_iterated_to())) + * value.count_at_value() + }) + .sum() + } + + /// Returns how many whole frame budgets `foreground_time` exceeded the + /// per frame budget by. This is a synthetic proxy for missed frames: the + /// benchmark harness has no vsync, so it counts how many frame deadlines + /// would have elapsed while the foreground thread was busy. + fn budget_overruns(&self, foreground_time: Duration) -> u64 { + let foreground_nanos = foreground_time.as_nanos(); + if foreground_nanos <= self.frame_budget_nanos { + return 0; + } + + let over_budget_nanos = foreground_nanos - self.frame_budget_nanos; + over_budget_nanos.div_ceil(self.frame_budget_nanos) as u64 + } + + /// Returns aggregate statistics for total foreground executor work + /// observed during the measured interval: every task poll, action + /// handler, and input dispatch on the foreground thread, whether or not + /// it produced a window draw. This is captured through GPUI's foreground + /// journal, so it requires no window and surfaces a slow or stalled task + /// even when nothing was drawn while it ran. + /// + /// Returns `None` when no foreground work was recorded, e.g. a + /// [`BenchAppContext::bench_iter`] measurement that does no async work. + pub fn foreground_work(&self) -> Option { + let frame_snapshot = self.frame_snapshot.borrow(); + let foreground_work = &frame_snapshot.foreground_work; + if foreground_work.histogram.is_empty() { + return None; + } + + let max = Duration::from_nanos(foreground_work.histogram.max()); + Some(ForegroundWorkSummary { + count: foreground_work.histogram.len(), + total: Duration::from_nanos(foreground_work.total_nanos), + max, + p50: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.50)), + p90: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.90)), + p95: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.95)), + p99: Duration::from_nanos(foreground_work.histogram.value_at_quantile(0.99)), + frame_budget_overruns_total: self.total_budget_overruns(&foreground_work.histogram), + frame_budget_overruns_max: self.budget_overruns(max), + }) + } + + /// Prints this report to stderr. + pub fn print(&self, benchmark_name: &str) { + let frame_snapshot = self.frame_snapshot.borrow(); + if frame_snapshot.is_empty() && self.metrics.is_empty() { + return; + } + + eprintln!("GPUI bench report (all observed iterations): {benchmark_name}"); + eprintln!(" note: includes Criterion warmup/calibration"); + self.metrics.print(" "); + self.print_histogram("window dirty-to-draw", &frame_snapshot.dirty_to_draw); + self.print_histogram("window draw", &frame_snapshot.draw); + self.print_histogram("window present interval", &frame_snapshot.present_interval); + if !frame_snapshot.invalidations_per_frame.is_empty() { + eprintln!( + " invalidations per frame: mean {:.2}, max {}", + frame_snapshot.invalidations_per_frame.mean(), + frame_snapshot.invalidations_per_frame.max() + ); + } + self.print_foreground_work(&frame_snapshot.foreground_work); + } + + fn print_histogram(&self, name: &str, histogram: &Histogram) { + if histogram.is_empty() { + return; + } + + eprintln!(" {name}:"); + self.print_histogram_body(histogram); + } + + fn print_foreground_work(&self, foreground_work: &DurationHistogram) { + if foreground_work.histogram.is_empty() { + return; + } + + eprintln!(" foreground executor work (task polls, actions, input dispatch):"); + eprintln!(" note: excludes window draw/present, reported separately above"); + eprintln!( + " total: {}", + format_duration(Duration::from_nanos(foreground_work.total_nanos)) + ); + self.print_histogram_body(&foreground_work.histogram); + } + + fn print_histogram_body(&self, histogram: &Histogram) { + let max_foreground_time = Duration::from_nanos(histogram.max()); + eprintln!(" samples: {}", histogram.len()); + eprintln!( + " mean: {}", + format_duration(Duration::from_nanos(histogram.mean() as u64)) + ); + eprintln!( + " p50: {}", + format_duration(Duration::from_nanos(histogram.value_at_quantile(0.50))) + ); + eprintln!( + " p90: {}", + format_duration(Duration::from_nanos(histogram.value_at_quantile(0.90))) + ); + eprintln!( + " p95: {}", + format_duration(Duration::from_nanos(histogram.value_at_quantile(0.95))) + ); + eprintln!( + " p99: {}", + format_duration(Duration::from_nanos(histogram.value_at_quantile(0.99))) + ); + eprintln!(" max: {}", format_duration(max_foreground_time)); + eprintln!( + " frame budget overruns total: {}", + self.total_budget_overruns(histogram) + ); + eprintln!( + " frame budget overruns max: {}", + self.budget_overruns(max_foreground_time) + ); + } +} + +struct WindowFrameSnapshot { + dirty_to_draw: Histogram, + draw: Histogram, + present_interval: Histogram, + invalidations_per_frame: Histogram, + foreground_work: DurationHistogram, +} + +impl WindowFrameSnapshot { + fn new() -> Self { + Self { + dirty_to_draw: Histogram::new(3).expect("3 significant digits is valid"), + draw: Histogram::new(3).expect("3 significant digits is valid"), + present_interval: Histogram::new(3).expect("3 significant digits is valid"), + invalidations_per_frame: Histogram::new(3).expect("3 significant digits is valid"), + foreground_work: DurationHistogram::new(), + } + } + + fn is_empty(&self) -> bool { + self.dirty_to_draw.is_empty() + && self.draw.is_empty() + && self.present_interval.is_empty() + && self.foreground_work.histogram.is_empty() + } +} + +/// A duration histogram paired with an exact running total, since the +/// histogram's bucketed values (3 significant digits) approximate a sum less +/// precisely than tracking it directly. +struct DurationHistogram { + histogram: Histogram, + total_nanos: u64, +} + +impl DurationHistogram { + fn new() -> Self { + Self { + histogram: Histogram::new(3).expect("3 significant digits is valid"), + total_nanos: 0, + } + } + + fn record(&mut self, duration: Duration) { + let nanos = duration.as_nanos() as u64; + // Infallible: the histogram auto-resizes. + self.histogram.record(nanos).ok(); + self.total_nanos += nanos; + } +} + +fn format_duration(duration: Duration) -> String { + format!("{:.3}ms", duration.as_secs_f64() * 1000.) +} + +/// Enables profiler tracing for a measurement and collects its frame events +/// and foreground journal entries. +/// +/// The previous tracing state is restored on drop, so a panicking measurement +/// doesn't leave tracing enabled for unrelated code such as a later benchmark +/// in the same process. +/// +/// The foreground journal collector is created at the same point, so +/// foreground work recorded before the scope starts (e.g. per-iteration +/// setup) is excluded from what [`Self::finish`] returns: a collector only +/// observes entries recorded after its creation. +struct TraceScope { + collector: FrameTimingCollector, + journal_collector: ForegroundJournalCollector, + _trace_guard: profiler::TraceGuard, +} + +impl TraceScope { + fn start(journal_collector: ForegroundJournalCollector) -> Self { + let trace_guard = profiler::trace_scope(); + Self { + collector: FrameTimingCollector::new(), + journal_collector, + _trace_guard: trace_guard, + } + } + + fn finish(mut self) -> TracedEvents { + TracedEvents { + frame_events: self.collector.collect_unseen(), + journal_entries: self.journal_collector.collect_unseen().entries, + } + } +} + +/// Events observed during one [`TraceScope`]. +struct TracedEvents { + frame_events: Vec, + journal_entries: Vec, +} + +impl TracedEvents { + /// Foreground journal entries that describe completed work (task polls, + /// action handlers, input dispatches, draws, presents, and folded + /// sub-floor polls), excluding interval boundaries and metadata. + fn foreground_events(&self) -> impl Iterator { + self.journal_entries.iter().filter_map(|entry| match entry { + ForegroundJournalEntry::Event(event) => Some(event), + _ => None, + }) + } +} + +struct MeasuredTaskInput { + input: Input, + trace_scope: Option, +} + +struct MeasuredTaskOutput { + trace_scope: Option, + report: BenchReport, + _output: Output, +} + +impl Drop for MeasuredTaskOutput { + fn drop(&mut self) { + let trace_scope = self + .trace_scope + .take() + .expect("measured task output should retain its trace scope"); + let events = trace_scope.finish(); + self.report.record_frame_timings(events.frame_events.iter()); + self.report + .record_foreground_events(events.foreground_events()); + } +} + +#[cfg(test)] +fn run_task_to_completion( + foreground_executor: &ForegroundExecutor, + task: Task, +) -> Output +where + Output: 'static, +{ + let output = Rc::new(RefCell::new(None)); + foreground_executor + .spawn({ + let output = output.clone(); + async move { + *output.borrow_mut() = Some(task.await); + } + }) + .detach(); + + foreground_executor + .dispatcher() + .as_threaded() + .expect("BenchAppContext requires a ThreadedDispatcher") + .run_until(|| output.borrow_mut().take()) +} + +/// A GPUI app context for Criterion benchmarks. +/// +/// `BenchAppContext` is intentionally separate from `TestAppContext`: it owns a +/// benchmark app instance and exposes only the app/window operations needed by +/// benchmark setup. Criterion remains responsible for the measured loop via its +/// `Bencher` API. +#[derive(Clone)] +pub struct BenchAppContext<'a, 'measurement> { + app: Rc, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + benchmark_name: Option<&'static str>, + bencher: Rc>>>, + report: BenchReport, +} + +impl<'a, 'measurement> BenchAppContext<'a, 'measurement> { + /// Creates a new benchmark app context backed by the provided platform. + /// + /// The platform's executors must be backed by a [`ThreadedDispatcher`] + /// (see [`bench_platform`]) so the context can drain foreground work via + /// [`Self::run_until_idle`]; panics otherwise. + pub fn new( + platform: Rc, + benchmark_name: Option<&'static str>, + bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>, + ) -> Self { + Self::build(platform, benchmark_name, bencher, BenchReport::default()) + } + + /// Creates a new benchmark app context backed by the provided platform. + /// + /// The platform's executors must be backed by a [`ThreadedDispatcher`] + /// (see [`bench_platform`]) so the context can drain foreground work via + /// [`Self::run_until_idle`]; panics otherwise. + #[doc(hidden)] + pub fn new_with_platform_and_report( + platform: Rc, + benchmark_name: Option<&'static str>, + bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>, + report: BenchReport, + ) -> Self { + Self::build(platform, benchmark_name, bencher, report) + } + + fn build( + platform: Rc, + benchmark_name: Option<&'static str>, + bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>, + report: BenchReport, + ) -> Self { + let background_executor = platform.background_executor(); + // Validate up front so misconfiguration fails at construction with a + // clear message instead of deep inside `run_until_idle`. + assert!( + background_executor.dispatcher().as_threaded().is_some(), + "BenchAppContext requires a platform whose executors are backed by a \ + ThreadedDispatcher; construct one with gpui::bench_platform" + ); + let foreground_executor = platform.foreground_executor(); + let asset_source = Arc::new(()); + // Benchmark setup must not make accidental network requests. The + // production `BlockedHttpClient` reports them without enabling a + // configurable test double through `test-support`. + let http_client: Arc = + Arc::new(http_client::BlockedHttpClient::new()); + let app = App::new_app(platform, asset_source, http_client); + + Self { + app, + background_executor, + foreground_executor, + benchmark_name, + bencher: Rc::new(RefCell::new(Some(bencher))), + report, + } + } + + /// The benchmark function name that created this context. + pub fn benchmark_name(&self) -> Option<&'static str> { + self.benchmark_name + } + + /// Returns the background executor used by this benchmark app. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Returns the foreground executor used by this benchmark app. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Updates the app and flushes synchronous GPUI effects afterward. + pub fn update(&mut self, update: impl FnOnce(&mut App) -> R) -> R { + let mut app = self.app.borrow_mut(); + app.update(update) + } + + /// Reads app state. + pub fn read(&self, read: impl FnOnce(&App) -> R) -> R { + let app = self.app.borrow(); + read(&app) + } + + /// Runs queued foreground tasks on this thread and waits for in flight + /// background work to finish. Timers that aren't due yet are not waited + /// for (see [`ThreadedDispatcher::run_until_idle`]). Scheduled frames are + /// delivered after each task poll and when there are no ready tasks, but + /// animations alone do not keep this method running. + pub fn run_until_idle(&self) { + let dispatcher = self + .background_executor + .dispatcher() + .as_threaded() + .expect("validated in BenchAppContext::build"); + dispatcher.run_until_idle_with(|| { + let ran_tasks = dispatcher.run_ready_main_tasks_with( + || true, + || { + self.dispatch_pending_frames(|| true); + }, + ); + if !ran_tasks { + self.dispatch_pending_frames(|| true); + } + ran_tasks + }); + } + + /// Alternates draining queued work with GPUI update cycles until neither + /// makes progress, so state dropped by benchmark code is fully released. + /// + /// Dropped entities are released only inside an update's effect flush, and + /// releases cascade: one flush drops the entities whose handles are gone, + /// their drops release further handles and can queue foreground work, and + /// a later flush collects those. Executor pumping alone never runs a + /// flush, so without this dropped state would linger in the entity map + /// until some woken task happened to run an update. Production gets this + /// cadence for free from frames and input events. + pub fn settle(&mut self) { + let dispatcher = self.background_executor.dispatcher().clone(); + let dispatcher = dispatcher + .as_threaded() + .expect("validated in BenchAppContext::build"); + loop { + self.run_until_idle(); + self.update(|_| ()); + self.dispatch_pending_frames(|| true); + if dispatcher.is_idle() { + return; + } + } + } + + /// Runs main-thread tasks until `ready` returns a value. + /// + /// While incomplete, scheduled frames run after each task poll and while + /// waiting for work. Readiness is checked before frame delivery, so completion + /// does not wait for a final presentation; renderer sessions do that explicitly. + pub fn run_until(&self, ready: impl FnMut() -> Option) -> R { + self.background_executor + .dispatcher() + .as_threaded() + .expect("validated in BenchAppContext::build") + .run_until_with_frames(ready, || self.dispatch_pending_frames(|| true)) + } + + /// Creates a collector observing foreground journal entries recorded + /// from this point on, for use by a new [`TraceScope`]. + fn foreground_journal_collector(&self) -> ForegroundJournalCollector { + self.read(|app| app.foreground_journal().collector()) + } + + /// Measures a generic benchmark workload using Criterion's iteration loop. + /// + /// The closure is invoked once per Criterion iteration with this + /// benchmark app context so it can update GPUI state. Each iteration then + /// delivers the scheduled frame batch, without draining foreground tasks. + /// + /// Any window draws triggered by the workload are recorded into the + /// benchmark's frame report through the GPUI frame profiler. + pub fn bench_iter(&mut self, mut benchmark: impl FnMut(&mut Self)) { + let bencher = self.take_bencher("bench_iter"); + self.dispatch_pending_frames(|| true); + let collector = TraceScope::start(self.foreground_journal_collector()); + self.report.metrics.discard_pending(); + let mut iterations = 0; + let mut benchmark = || { + iterations += 1; + benchmark(self); + self.dispatch_pending_frames(|| true); + }; + bencher.iter(&mut benchmark); + let events = collector.finish(); + self.report.metrics.record_sample(iterations); + self.report.record_frame_timings(events.frame_events.iter()); + self.report + .record_foreground_events(events.foreground_events()); + self.replace_bencher(bencher); + } + + /// Measures a GPUI task to completion using Criterion's iteration loop. + /// + /// The closure is invoked once per Criterion iteration. The returned task + /// may depend on foreground work, background work, timers, or external + /// workers that wake GPUI tasks. Its output is dropped after the timed + /// interval. + /// + /// Any window draws triggered by the task are recorded into the benchmark's + /// frame report through the GPUI frame profiler. + pub fn bench_task(&mut self, mut benchmark: impl FnMut(&mut Self) -> Task) + where + Output: 'static, + { + self.bench_batched_task_internal("bench_task", |_| (), |_, cx| benchmark(cx)); + } + + /// Measures a GPUI task with per-iteration setup outside the timed interval. + /// + /// `setup` runs before timing starts. The returned input is passed by mutable + /// reference to `benchmark`, which returns the task whose completion is + /// measured. Both the setup input and task output are dropped after timing + /// stops. + /// + /// Each iteration is kept in its own Criterion batch so profiler tracing and + /// destruction cannot overlap adjacent measurements. + pub fn bench_batched_task( + &mut self, + setup: impl FnMut(&mut Self) -> Input, + benchmark: impl FnMut(&mut Input, &mut Self) -> Task, + ) where + Output: 'static, + { + self.bench_batched_task_internal("bench_batched_task", setup, benchmark); + } + + fn bench_batched_task_internal( + &mut self, + benchmark_kind: &str, + mut setup: impl FnMut(&mut Self) -> Input, + mut benchmark: impl FnMut(&mut Input, &mut Self) -> Task, + ) where + Output: 'static, + { + let bencher = self.take_bencher(benchmark_kind); + let mut setup_context = self.clone(); + let mut benchmark_context = self.clone(); + let foreground_executor = self.foreground_executor.clone(); + let report = self.report.clone(); + report.metrics.discard_pending(); + let iterations = std::cell::Cell::new(0); + + bencher.iter_batched_ref( + || { + // The previous iteration's input and output were just + // dropped; settling here releases their entities before the + // next setup, so per-iteration state cannot accumulate + // across a measurement. + setup_context.settle(); + let input = setup(&mut setup_context); + setup_context.dispatch_pending_frames(|| true); + MeasuredTaskInput { + input, + trace_scope: Some(TraceScope::start( + setup_context.foreground_journal_collector(), + )), + } + }, + |measured_input| { + iterations.set(iterations.get() + 1); + let task = benchmark(&mut measured_input.input, &mut benchmark_context); + benchmark_context.dispatch_pending_frames(|| true); + let output = Rc::new(RefCell::new(None)); + let completion = foreground_executor.spawn({ + let output = output.clone(); + async move { + *output.borrow_mut() = Some(task.await); + } + }); + let output = benchmark_context.run_until(|| output.borrow_mut().take()); + drop(completion); + MeasuredTaskOutput { + trace_scope: measured_input.trace_scope.take(), + report: report.clone(), + _output: output, + } + }, + criterion::BatchSize::PerIteration, + ); + report.metrics.record_sample(iterations.get()); + self.replace_bencher(bencher); + } + + /// Measures unpaced update and rendering work for an entity in its current window. + /// + /// Each iteration runs `update` against the entity in its current window. In + /// renderer measurements, effects flush without drawing until the platform + /// frame callback. The entity should be part of the window's render tree, such as the + /// root view or a child of it. + /// + /// Each iteration first pumps at most the initial ready task count, delivering + /// pending frames after every poll. It then runs `update` and delivers pending + /// frames again. Nested actions and effects finish before frame delivery. + /// This measures work and rendering cost, not display latency: there is no + /// pacing, and the report's frame budget does not control execution. + /// + /// Frame events are collected through the GPUI frame profiler + /// ([`crate::profiler::record_frame_event`]), which is enabled for the + /// duration of the measurement. + pub fn bench_renderer( + &mut self, + view: Entity, + mut update: impl FnMut(&mut V, &mut Window, &mut Context), + ) where + V: 'static + Render, + { + let bencher = self.take_bencher("bench_renderer"); + let dispatcher = self.background_executor.dispatcher().clone(); + self.dispatch_pending_frames(|| true); + let collector = TraceScope::start(self.foreground_journal_collector()); + self.report.metrics.discard_pending(); + let mut iterations = 0; + let mut benchmark = || { + iterations += 1; + dispatcher + .as_threaded() + .expect("validated in BenchAppContext::build") + .run_ready_main_tasks_with( + || true, + || { + self.dispatch_pending_frames(|| true); + }, + ); + self.with_window(view.entity_id(), |window, cx| { + view.update(cx, |view, cx| update(view, window, cx)); + }) + .expect("cannot benchmark renderer for entity without a current window"); + self.dispatch_pending_frames(|| true); + }; + bencher.iter(&mut benchmark); + + let events = collector.finish(); + self.report.metrics.record_sample(iterations); + self.report.record_frame_timings(events.frame_events.iter()); + self.report + .record_foreground_events(events.foreground_events()); + self.replace_bencher(bencher); + } + + /// Measures finite rendering sessions with fresh, untimed setup for each iteration. + /// + /// `setup` returns session state, its window, and a shared stop flag. + /// Loop turns pump at most the initial ready foreground task count, then call + /// `input` with a zero-based turn number unless stopped. Pending frames are + /// delivered after each task poll and input callback. Between polls, the stop + /// flag and session deadline are checked. The ready batch snapshots a count, + /// not task identities; rendering after a poll does not inject another input. + /// This is an unpaced work-and-render policy, not OS event-loop emulation. + /// Once stopped, only scheduled platform frame callbacks run until the window + /// is clean and its final changes have been submitted for presentation. + /// Pending animation callbacks alone do not delay completion. + /// Turns run without pacing or explicit OS-thread yields. A clean turn need not draw. + /// + /// Store `true` with release ordering to stop. Stopping does not imply success: + /// validate the workload in `Input::drop` or retained fixture state afterward. + /// Setup, session state destruction, and report aggregation are outside both + /// timing and tracing. Own outstanding tasks in `Input` so dropping it cancels + /// them; do not detach session work that could leak into subsequent iterations. + /// + /// # Panics + /// + /// Panics if the window is removed or stopping and final presentation exceed `timeout`. + /// The deadline is checked between polls and frames and cannot preempt a blocking task + /// poll, input callback, draw, or present. + pub fn bench_renderer_session( + &mut self, + timeout: Duration, + mut setup: impl FnMut(&mut Self) -> (Input, AnyWindowHandle, Arc), + mut input: impl FnMut(&mut Input, u64, &mut Window, &mut App), + ) { + let bencher = self.take_bencher("bench_renderer_session"); + let mut setup_context = self.clone(); + let mut benchmark_context = self.clone(); + let dispatcher = self.background_executor.dispatcher().clone(); + let dispatcher = dispatcher + .as_threaded() + .expect("validated in BenchAppContext::build"); + let report = self.report.clone(); + report.metrics.discard_pending(); + let iterations = std::cell::Cell::new(0); + + bencher.iter_batched_ref( + || { + setup_context.settle(); + let input = setup(&mut setup_context); + setup_context.dispatch_pending_frames(|| true); + MeasuredTaskInput { + input, + trace_scope: Some(TraceScope::start( + setup_context.foreground_journal_collector(), + )), + } + }, + |measured_input| { + iterations.set(iterations.get() + 1); + let (state, window, stopped) = &mut measured_input.input; + let started = Instant::now(); + let check_deadline = || { + assert!( + started.elapsed() < timeout, + "renderer session did not stop within {timeout:?} with final changes presented" + ); + }; + let can_poll = || { + check_deadline(); + !stopped.load(Ordering::Acquire) + }; + let is_finished = |cx: &Self| { + if !stopped.load(Ordering::Acquire) { + return false; + } + // Animation callbacks alone must not keep a stopped session alive. + let app = cx.app.borrow(); + let window = app + .windows + .get(window.window_id()) + .and_then(Option::as_deref) + .expect("renderer session window must remain open"); + !window.invalidator.is_dirty() && !window.needs_present.get() + }; + let dispatch_frames = |cx: &Self| { + check_deadline(); + cx.dispatch_pending_frames(|| { + check_deadline(); + !is_finished(cx) + }); + check_deadline(); + }; + let mut frame = 0; + loop { + check_deadline(); + if is_finished(&benchmark_context) { + break; + } + if can_poll() { + dispatcher.run_ready_main_tasks_with(&can_poll, || { + dispatch_frames(&benchmark_context); + }); + if !can_poll() { + continue; + } + benchmark_context + .update_window(*window, |_, window, cx| { + if can_poll() { + input(state, frame, window, cx); + } + }) + .expect("renderer session window must remain open"); + } + dispatch_frames(&benchmark_context); + frame += 1; + } + MeasuredTaskOutput { + trace_scope: measured_input.trace_scope.take(), + report: report.clone(), + _output: (), + } + }, + criterion::BatchSize::PerIteration, + ); + report.metrics.record_sample(iterations.get()); + self.replace_bencher(bencher); + } + + fn dispatch_pending_frames(&self, mut should_continue: impl FnMut() -> bool) -> bool { + let pending: Vec<_> = { + let mut app = self.app.borrow_mut(); + app.windows + .values_mut() + .filter_map(|window| { + let window = window.as_deref_mut()?; + let handle = window.window_handle(); + let platform_window = window + .platform_window + .as_test() + .expect("benchmark platform window"); + platform_window + .frame_scheduled() + .then(|| (handle, platform_window.clone())) + }) + .collect() + }; + + let mut dispatched = false; + for (handle, window) in pending { + if !should_continue() { + break; + } + // An earlier callback may have closed another window in the batch. + let is_open = self + .app + .borrow() + .windows + .get(handle.window_id()) + .and_then(Option::as_deref) + .is_some(); + if is_open { + dispatched |= window.simulate_scheduled_frame(); + } + } + dispatched + } + + /// Adds an active window with an empty root view for benchmark setup. + /// + /// Activation is settled before returning so renderer measurements exercise + /// foreground animation rather than the inactive-window frame throttle. + pub fn add_empty_window(&mut self) -> BenchWindowContext<'a, 'measurement> { + let bounds = { + let app = self.app.borrow(); + Bounds::maximized(None, &app) + }; + let window = { + let mut app = self.app.borrow_mut(); + let window: AnyWindowHandle = app + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Empty), + ) + .expect("failed to open benchmark window") + .into(); + // An active renderer workload must not inherit the platform + // callback's inactive-window animation throttle. + app.update_window(window, |_, window, _| window.activate_window()) + .expect("failed to activate newly created benchmark window"); + window + }; + + self.run_until_idle(); + BenchWindowContext { + cx: self.clone(), + window, + } + } + + fn take_bencher( + &self, + benchmark_kind: &str, + ) -> &'a mut criterion::Bencher<'measurement, BenchMeasurement> { + self.bencher.borrow_mut().take().unwrap_or_else(|| { + panic!("cannot start {benchmark_kind}: benchmark measurement is already running") + }) + } + + fn replace_bencher(&self, bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>) { + let previous = self.bencher.borrow_mut().replace(bencher); + assert!( + previous.is_none(), + "benchmark bencher was unexpectedly present after measurement" + ); + } + + /// Runs GPUI benchmark teardown. + /// + /// Cancels any timers still armed on the shared dispatcher and drains the + /// work that cancellation unblocks so they can't fire during a later + /// benchmark; assumes no other `BenchAppContext` is live on this thread. + pub fn teardown(mut self) { + self.run_until_idle(); + self.update(|cx| { + cx.quit(); + }); + self.run_until_idle(); + + let dispatcher = self.background_executor.dispatcher(); + let dispatcher = dispatcher + .as_threaded() + .expect("validated in BenchAppContext::build"); + + drop(self.app); + drop(self.foreground_executor); + + for _ in 0..100 { + if dispatcher.cancel_pending_timers() == 0 { + return; + } + dispatcher.run_until_idle(); + } + panic!( + "benchmark teardown kept scheduling timers: {}", + dispatcher.debug_state() + ); + } +} + +impl AppContext for BenchAppContext<'_, '_> { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let mut app = self.app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> Reservation { + let mut app = self.app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let mut app = self.app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let mut app = self.app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'b, T>(&'b mut self, _: &Entity) -> GpuiBorrow<'b, T> + where + T: 'static, + { + panic!("Cannot use as_mut with BenchAppContext. Call update() instead.") + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let app = self.app.borrow(); + app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut app = self.app.borrow_mut(); + app.update_window(window, update) + } + + fn with_window( + &mut self, + entity_id: EntityId, + update: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let mut app = self.app.borrow_mut(); + app.with_window(entity_id, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let app = self.app.borrow(); + app.read_global(callback) + } +} + +/// A window-specific context for GPUI benchmarks. +/// +/// This is separate from `VisualTestContext`; it provides access to a benchmark +/// window without exposing test-only helpers such as input simulation. +#[derive(Clone)] +pub struct BenchWindowContext<'a, 'measurement> { + cx: BenchAppContext<'a, 'measurement>, + window: AnyWindowHandle, +} + +impl<'a, 'measurement> BenchWindowContext<'a, 'measurement> { + /// Returns the underlying benchmark app context. + pub fn app_context(&mut self) -> &mut BenchAppContext<'a, 'measurement> { + &mut self.cx + } + + /// Returns the window associated with this context. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + /// Runs queued foreground tasks on this thread and waits for in-flight + /// background work to finish. Pending timers are not waited for. + pub fn run_until_idle(&self) { + self.cx.run_until_idle(); + } + + /// Updates the benchmark window. + pub fn update(&mut self, update: impl FnOnce(&mut Window, &mut App) -> R) -> R { + self.cx + .update_window(self.window, |_, window, cx| update(window, cx)) + .expect("benchmark window was unexpectedly closed") + } +} + +impl AppContext for BenchWindowContext<'_, '_> { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + self.window + .update(&mut self.cx, |_, _, cx| cx.new(build_entity)) + .expect("benchmark window was unexpectedly closed") + } + + fn reserve_entity(&mut self) -> Reservation { + self.cx.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + self.window + .update(&mut self.cx, |_, _, cx| { + cx.insert_entity(reservation, build_entity) + }) + .expect("benchmark window was unexpectedly closed") + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + self.cx.update_entity(handle, update) + } + + fn as_mut<'b, T>(&'b mut self, handle: &Entity) -> GpuiBorrow<'b, T> + where + T: 'static, + { + self.cx.as_mut(handle) + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + self.cx.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.cx.update_window(window, update) + } + + fn with_window( + &mut self, + entity_id: EntityId, + update: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + self.cx.with_window(entity_id, update) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.cx.background_spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + self.cx.read_global(callback) + } +} + +impl VisualContext for BenchWindowContext<'_, '_> { + type Result = Result; + + fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + fn update_window_entity( + &mut self, + entity: &Entity, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Result { + let entity = entity.clone(); + self.cx + .app + .borrow_mut() + .with_window(entity.entity_id(), |window, app| { + entity.update(app, |entity, cx| update(entity, window, cx)) + }) + .ok_or_else(|| { + anyhow!("entity has no current window; use `update` instead of `update_in`") + }) + } + + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Result> { + self.window.update(&mut self.cx, |_, window, cx| { + cx.new(|cx| build_entity(window, cx)) + }) + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Result> + where + V: 'static + Render, + { + self.window.update(&mut self.cx, |_, window, cx| { + window.replace_root(cx, build_view) + }) + } + + fn focus(&mut self, entity: &Entity) -> Result<()> + where + V: Focusable, + { + self.window.update(&mut self.cx, |_, window, cx| { + entity.read(cx).focus_handle(cx).focus(window, cx) + }) + } +} + +#[cfg(test)] +mod tests { + use std::{ + rc::Rc, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, + }; + + use super::*; + use crate::profiler::journal::install_test_foreground_journal; + + /// Runs `benchmark` under a wall-time primary with a fake counter as the + /// secondary metric and returns the per-iteration values the report + /// recorded for it. + fn secondary_values_per_iteration( + counter: Arc, + benchmark: impl Fn(&mut BenchAppContext, &Arc), + ) -> Vec { + let platform = bench_platform(None, Arc::new(crate::NoopTextSystem::new())); + let report = BenchReport::default(); + let mut criterion = criterion::Criterion::default() + .with_measurement( + BenchMeasurement::new(criterion::measurement::WallTime).with_secondary( + "fake counter", + bench_metrics::FakeCounter::new(counter.clone()), + ), + ) + .without_plots() + .sample_size(10) + .warm_up_time(Duration::from_millis(1)) + .measurement_time(Duration::from_millis(1)); + + criterion.bench_function("secondary_values_per_iteration", |bencher| { + let mut cx = BenchAppContext::new_with_platform_and_report( + platform.clone(), + Some("secondary_values_per_iteration"), + bencher, + report.clone(), + ); + // Fixture work outside the measured loop must not be attributed + // to any iteration. + counter.fetch_add(1_000_000, Ordering::SeqCst); + benchmark(&mut cx, &counter); + cx.teardown(); + }); + + report + .metrics() + .values("fake counter") + .expect("the fake counter was recorded") + .into_iter() + .map(|value| value as u64) + .collect() + } + + #[test] + fn secondary_metric_is_normalized_per_iteration_for_bench_iter() { + let values = secondary_values_per_iteration(Arc::new(AtomicU64::new(0)), |cx, counter| { + cx.bench_iter(|_| { + counter.fetch_add(11, Ordering::SeqCst); + }); + }); + assert!(!values.is_empty()); + assert!( + values.iter().all(|value| *value == 11), + "each sample's total should be divided by its iteration count: {values:?}" + ); + } + + #[test] + fn secondary_metric_is_accumulated_across_per_iteration_batches() { + let values = secondary_values_per_iteration(Arc::new(AtomicU64::new(0)), |cx, counter| { + cx.bench_batched_task( + |_| { + // Setup runs outside every measured interval. + counter.fetch_add(1_000, Ordering::SeqCst); + }, + |(), cx| { + counter.fetch_add(11, Ordering::SeqCst); + cx.background_spawn(async {}) + }, + ); + }); + assert!(!values.is_empty()); + assert!( + values.iter().all(|value| *value == 11), + "PerIteration batches should sum to one per-iteration value: {values:?}" + ); + } + + #[test] + fn foreground_work_reports_long_task_without_window_draw() { + let (journal, _journal_guard) = install_test_foreground_journal(1024, 64); + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + + let trace_scope = TraceScope::start(journal.collector()); + + // A single foreground task poll that never touches a window, akin + // to the stall a debounced background computation can cause. + let task = foreground_executor.spawn(async move { + std::thread::sleep(Duration::from_millis(60)); + }); + run_task_to_completion(&foreground_executor, task); + + let events = trace_scope.finish(); + assert!( + events.frame_events.is_empty(), + "no window was involved, so no frame events should be recorded" + ); + + let report = BenchReport::default(); + report.record_foreground_events(events.foreground_events()); + + let summary = report + .foreground_work() + .expect("a long task poll should be reported even without a window draw"); + // The spawned task's own poll is one sample; the tiny wrapper poll + // that observes its completion in `run_task_to_completion` folds + // into a second, near-zero sample rather than being dropped. + assert!(summary.count >= 1, "expected at least one recorded item"); + assert!( + summary.max >= Duration::from_millis(55), + "expected the long poll's duration to be recorded, got {:?}", + summary.max + ); + // `total` is an exact sum, while `max` may be rounded up to its + // histogram bucket's boundary, so compare each against the expected + // floor directly instead of against each other. + assert!( + summary.total >= Duration::from_millis(55), + "expected the long poll's duration to be included in the total, got {:?}", + summary.total + ); + } + + #[test] + fn foreground_work_excludes_setup_before_trace_scope_starts() { + let (journal, _journal_guard) = install_test_foreground_journal(1024, 64); + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + + // Fixture/setup work that must not be attributed to the measurement: + // a long poll recorded before the trace scope (and its journal + // collector) is created. + let setup_task = foreground_executor.spawn(async move { + std::thread::sleep(Duration::from_millis(80)); + }); + run_task_to_completion(&foreground_executor, setup_task); + + let trace_scope = TraceScope::start(journal.collector()); + + let measured_task = foreground_executor.spawn(async move { + std::thread::sleep(Duration::from_millis(10)); + }); + run_task_to_completion(&foreground_executor, measured_task); + + let events = trace_scope.finish(); + let report = BenchReport::default(); + report.record_foreground_events(events.foreground_events()); + + let summary = report + .foreground_work() + .expect("the measured task's poll should be reported"); + assert!( + summary.max < Duration::from_millis(40), + "setup work's 80ms poll must not leak into the measured summary, got {:?}", + summary.max + ); + assert!( + summary.total < Duration::from_millis(40), + "setup work's 80ms poll must not leak into the measured total, got {:?}", + summary.total + ); + } + + #[test] + fn bench_task_reports_long_task_without_window() { + let platform = bench_platform(None, Arc::new(crate::NoopTextSystem::new())); + let report = BenchReport::default(); + let name = "bench_task_reports_long_task_without_window"; + + let mut criterion = criterion::Criterion::default() + .with_measurement(BenchMeasurement::new(criterion::measurement::WallTime)) + .without_plots() + .sample_size(10) + .warm_up_time(Duration::from_millis(1)) + .measurement_time(Duration::from_millis(1)); + + criterion.bench_function(name, |bencher| { + let mut cx = BenchAppContext::new_with_platform_and_report( + platform.clone(), + Some(name), + bencher, + report.clone(), + ); + cx.bench_task(|cx| { + cx.foreground_executor().spawn(async move { + std::thread::sleep(Duration::from_millis(20)); + }) + }); + cx.teardown(); + }); + + let summary = report + .foreground_work() + .expect("bench_task should report foreground work with no window involved"); + assert!( + summary.max >= Duration::from_millis(15), + "expected a ~20ms task poll to be recorded, got {:?}", + summary.max + ); + } + + #[test] + fn task_completion_supports_non_send_foreground_output() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let background_executor = BackgroundExecutor::new(dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(dispatcher); + let (sender, receiver) = futures::channel::oneshot::channel(); + + background_executor + .spawn(async move { + sender + .send(()) + .expect("foreground receiver should remain alive"); + }) + .detach(); + let expected_output = Rc::new(42); + let task_output = expected_output.clone(); + let task = foreground_executor.spawn(async move { + receiver.await.expect("background task should send a value"); + task_output + }); + + let output = run_task_to_completion(&foreground_executor, task); + assert!( + Rc::ptr_eq(&output, &expected_output), + "task runner should preserve non-Send foreground output" + ); + } +} diff --git a/vendor/gpui-pre/src/app/context.rs b/vendor/gpui-pre/src/app/context.rs new file mode 100644 index 0000000..bce22b5 --- /dev/null +++ b/vendor/gpui-pre/src/app/context.rs @@ -0,0 +1,959 @@ +use crate::{ + AnyView, AnyWindowHandle, AppContext, AsyncApp, DispatchPhase, Effect, EntityId, EventEmitter, + FocusHandle, FocusOutEvent, Focusable, Global, KeystrokeObserver, Priority, Reservation, + SubscriberSet, Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle, + WindowVisibility, +}; +use anyhow::Result; +use futures::FutureExt; +use gpui_util::Deferred; +use std::{ + any::{Any, TypeId}, + borrow::{Borrow, BorrowMut}, + future::Future, + ops, + sync::Arc, +}; + +use super::{App, AsyncWindowContext, Entity, KeystrokeEvent}; + +/// The app context, with specialized behavior for the given entity. +pub struct Context<'a, T> { + app: &'a mut App, + entity_state: WeakEntity, +} + +impl<'a, T> ops::Deref for Context<'a, T> { + type Target = App; + + fn deref(&self) -> &Self::Target { + self.app + } +} + +impl<'a, T> ops::DerefMut for Context<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.app + } +} + +impl<'a, T: 'static> Context<'a, T> { + pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity) -> Self { + Self { app, entity_state } + } + + /// The entity id of the entity backing this context. + pub fn entity_id(&self) -> EntityId { + self.entity_state.entity_id + } + + /// Returns a handle to the entity belonging to this context. + pub fn entity(&self) -> Entity { + self.weak_entity() + .upgrade() + .expect("The entity must be alive if we have a entity context") + } + + /// Returns a weak handle to the entity belonging to this context. + pub fn weak_entity(&self) -> WeakEntity { + self.entity_state.clone() + } + + /// Arranges for the given function to be called whenever [`Context::notify`] is + /// called with the given entity. + pub fn observe( + &mut self, + entity: &Entity, + mut on_notify: impl FnMut(&mut T, Entity, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + W: 'static, + { + let observer = self.weak_entity(); + self.app.observe_internal(entity, move |entity, cx| { + invoke_observer(&observer, entity, cx, &mut on_notify) + }) + } + + /// Observe changes to ourselves + pub fn observe_self( + &mut self, + mut on_event: impl FnMut(&mut T, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + { + let this = self.entity(); + self.app.observe(&this, move |this, cx| { + this.update(cx, |this, cx| on_event(this, cx)) + }) + } + + /// Subscribe to an event type from another entity + pub fn subscribe( + &mut self, + entity: &Entity, + mut on_event: impl FnMut(&mut T, Entity, &Evt, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + T2: 'static + EventEmitter, + Evt: 'static, + { + let subscriber = self.weak_entity(); + self.app + .subscribe_internal(entity, move |entity, event, cx| { + invoke_subscriber(&subscriber, entity, event, cx, &mut on_event) + }) + } + + /// Subscribe to an event type from ourself + pub fn subscribe_self( + &mut self, + mut on_event: impl FnMut(&mut T, &Evt, &mut Context) + 'static, + ) -> Subscription + where + T: 'static + EventEmitter, + Evt: 'static, + { + let this = self.entity(); + self.app.subscribe(&this, move |this, evt, cx| { + this.update(cx, |this, cx| on_event(this, evt, cx)) + }) + } + + /// Register a callback to be invoked when GPUI releases this entity. + pub fn on_release(&self, on_release: impl FnOnce(&mut T, &mut App) + 'static) -> Subscription + where + T: 'static, + { + let (subscription, activate) = self.app.release_listeners.insert( + self.entity_state.entity_id, + Box::new(move |this, cx| { + let this = this.downcast_mut().expect("invalid entity type"); + on_release(this, cx); + }), + ); + activate(); + subscription + } + + /// Register a callback to be run on the release of another entity + pub fn observe_release( + &self, + entity: &Entity, + on_release: impl FnOnce(&mut T, &mut T2, &mut Context) + 'static, + ) -> Subscription + where + T: Any, + T2: 'static, + { + let entity_id = entity.entity_id(); + let this = self.weak_entity(); + let (subscription, activate) = self.app.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| on_release(this, entity, cx)); + } + }), + ); + activate(); + subscription + } + + /// Register a callback to for updates to the given global + pub fn observe_global( + &mut self, + mut f: impl FnMut(&mut T, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_entity(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()), + ); + self.defer(move |_| activate()); + subscription + } + + /// Register a callback to be invoked when the application is about to restart. + pub fn on_app_restart( + &self, + mut on_restart: impl FnMut(&mut T, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let handle = self.weak_entity(); + self.app.on_app_restart(move |cx| { + handle.update(cx, |entity, cx| on_restart(entity, cx)).ok(); + }) + } + + /// Arrange for the given function to be invoked whenever the application is quit. + /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. + pub fn on_app_quit( + &self, + mut on_quit: impl FnMut(&mut T, &mut Context) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + T: 'static, + { + let handle = self.weak_entity(); + self.app.on_app_quit(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; + } + } + .boxed_local() + }) + } + + /// Tell GPUI that this entity has changed and observers of it should be notified. + pub fn notify(&mut self) { + self.app.notify(self.entity_state.entity_id); + } + + /// Spawn the future returned by the given function. + /// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points. + /// The returned task must be held or detached. + #[track_caller] + #[inline(always)] + pub fn spawn(&self, f: AsyncFn) -> Task + where + T: 'static, + AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncApp) -> R + 'static, + R: 'static, + { + let this = self.weak_entity(); + self.app.spawn(async move |cx| f(this, cx).await) + } + + /// Convenience method for accessing view state in an event callback. + /// + /// Many GPUI callbacks take the form of `Fn(&E, &mut Window, &mut App)`, + /// but it's often useful to be able to access view state in these + /// callbacks. This method provides a convenient way to do so. + #[inline(always)] + pub fn listener( + &self, + listener: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, + ) -> impl Fn(&E, &mut Window, &mut App) + 'static { + let view = self.entity().downgrade(); + move |event: &E, window: &mut Window, cx: &mut App| { + invoke_listener(&view, window, cx, &|view, window, cx| { + listener(view, event, window, cx); + }); + } + } + + /// Convenience method for producing view state in a closure. + /// See `listener` for more details. + pub fn processor( + &self, + f: impl Fn(&mut T, E, &mut Window, &mut Context) -> R + 'static, + ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static { + let view = self.entity(); + move |e: E, window: &mut Window, cx: &mut App| { + view.update(cx, |view, cx| f(view, e, window, cx)) + } + } + + /// Run something using this entity and cx, when the returned struct is dropped + pub fn on_drop( + &self, + f: impl FnOnce(&mut T, &mut Context) + 'static, + ) -> Deferred { + let this = self.weak_entity(); + let mut cx = self.to_async(); + gpui_util::defer(move || { + this.update(&mut cx, f).ok(); + }) + } + + /// Focus the given view in the given window. View type is required to implement Focusable. + pub fn focus_view(&mut self, view: &Entity, window: &mut Window) { + window.focus(&view.focus_handle(self), self); + } + + /// Sets a given callback to be run on the next frame. + pub fn on_next_frame( + &self, + window: &mut Window, + f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, + ) where + T: 'static, + { + let view = self.entity(); + window.on_next_frame(move |window, cx| view.update(cx, |view, cx| f(view, window, cx))); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer_in( + &mut self, + window: &Window, + f: impl FnOnce(&mut T, &mut Window, &mut Context) + 'static, + ) { + let view = self.weak_entity(); + let entity_id = self.entity_id(); + self.ensure_window(entity_id, window.handle.id); + self.app.defer(move |cx| { + cx.with_window(entity_id, |window, cx| { + view.update(cx, |view, cx| f(view, window, cx)).ok(); + }); + }); + } + + /// Observe another entity for changes to its state, as tracked by [`Context::notify`]. + pub fn observe_in( + &mut self, + observed: &Entity, + window: &mut Window, + mut on_notify: impl FnMut(&mut T, Entity, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + V2: 'static, + T: 'static, + { + let observed_id = observed.entity_id(); + let observed = observed.downgrade(); + let observer = self.weak_entity(); + let observer_id = self.entity_id(); + self.ensure_window(observer_id, window.handle.id); + self.new_observer( + observed_id, + Box::new(move |cx| invoke_observer_in(&observer, &observed, cx, &mut on_notify)), + ) + } + + /// Subscribe to events emitted by another entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked with a reference to the current view, a handle to the emitting `Entity`, the event, a mutable reference to the `Window`, and the context for the entity. + pub fn subscribe_in( + &mut self, + emitter: &Entity, + window: &Window, + mut on_event: impl FnMut(&mut T, &Entity, &Evt, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + Emitter: EventEmitter, + Evt: 'static, + { + let emitter = emitter.downgrade(); + let subscriber = self.weak_entity(); + let subscriber_id = self.entity_id(); + self.ensure_window(subscriber_id, window.handle.id); + self.new_subscription( + emitter.entity_id(), + ( + TypeId::of::(), + Box::new(move |event, cx| { + invoke_subscriber_in(&subscriber, &emitter, event, cx, &mut on_event) + }), + ), + ) + } + + /// Register a callback to be invoked when the view is released. + /// + /// The callback receives a handle to the view's window. This handle may be + /// invalid, if the window was closed before the view was released. + pub fn on_release_in( + &mut self, + window: &Window, + on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription { + let entity = self.entity(); + self.app.observe_release_in(&entity, window, on_release) + } + + /// Register a callback to be invoked when the given Entity is released. + pub fn observe_release_in( + &self, + observed: &Entity, + window: &Window, + mut on_release: impl FnMut(&mut T, &mut T2, &mut Window, &mut Context) + 'static, + ) -> Subscription + where + T: 'static, + T2: 'static, + { + let observer = self.weak_entity(); + self.app + .observe_release_in(observed, window, move |observed, window, cx| { + observer + .update(cx, |observer, cx| { + on_release(observer, observed, window, cx) + }) + .ok(); + }) + } + + /// Register a callback to be invoked when the window is resized. + pub fn observe_window_bounds( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.bounds_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Register a callback to be invoked when the window is activated or deactivated. + pub fn observe_window_activation( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.activation_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window's visibility changes + /// (see [`WindowVisibility`]). + pub fn observe_window_visibility( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, WindowVisibility, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.visibility_observers.insert( + (), + Box::new(move |visibility, window, cx| { + view.update(cx, |view, cx| callback(view, visibility, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window appearance changes. + pub fn observe_window_appearance( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.appearance_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window button layout changes. + pub fn observe_button_layout_changed( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.button_layout_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Registers a callback for [`App::observe_keystrokes`] that updates this entity. + pub fn observe_keystrokes( + &mut self, + mut f: impl FnMut(&mut T, &KeystrokeEvent, &mut Window, &mut Context) + 'static, + ) -> Subscription { + fn inner( + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_observers.insert((), handler); + activate(); + subscription + } + + let view = self.weak_entity(); + inner( + &self.keystroke_observers, + Box::new(move |event, window, cx| { + if let Some(view) = view.upgrade() { + view.update(cx, |view, cx| f(view, event, window, cx)); + true + } else { + false + } + }), + ) + } + + /// Register a callback to be invoked when the window's pending input changes. + pub fn observe_pending_input( + &self, + window: &mut Window, + mut callback: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.pending_input_observers.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| callback(view, window, cx)) + .is_ok() + }), + ); + activate(); + subscription + } + + /// Register a listener to be called when the given focus handle receives focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() != Some(&focus_id) + && event.current_focus_path.last() == Some(&focus_id) + { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// This does not fire if the given focus handle - or one of its descendants - was previously focused. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.is_focus_in(focus_id) { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_blur( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if event.previous_focus_path.last() == Some(&focus_id) + && event.current_focus_path.last() != Some(&focus_id) + { + listener(view, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when nothing in the window has focus. + /// This typically happens when the node that was focused is removed from the tree, + /// and this callback lets you chose a default place to restore the users focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_lost( + &mut self, + window: &mut Window, + mut listener: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let (subscription, activate) = window.focus_lost_listeners.insert( + (), + Box::new(move |window, cx| { + view.update(cx, |view, cx| listener(view, window, cx)) + .is_ok() + }), + ); + self.defer(|_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + window: &mut Window, + mut listener: impl FnMut(&mut T, FocusOutEvent, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let view = self.weak_entity(); + let focus_id = handle.id; + let (subscription, activate) = + window.new_focus_listener(Box::new(move |event, window, cx| { + view.update(cx, |view, cx| { + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(view, event, window, cx) + } + }) + .is_ok() + })); + self.defer(|_| activate()); + subscription + } + + /// Schedule a future to be run asynchronously. + /// The given callback is invoked with a [`WeakEntity`] to avoid leaking the entity for a long-running process. + /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points. + /// The returned future will be polled on the main thread. + #[track_caller] + #[inline(always)] + pub fn spawn_in(&self, window: &Window, f: AsyncFn) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncWindowContext) -> R + 'static, + { + let view = self.weak_entity(); + window.spawn(self, async move |cx| f(view, cx).await) + } + + /// Schedule a future to be run asynchronously with the given priority. + /// The given callback is invoked with a [`WeakEntity`] to avoid leaking the entity for a long-running process. + /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the entity across await points. + /// The returned future will be polled on the main thread. + #[track_caller] + #[inline(always)] + pub fn spawn_in_with_priority( + &self, + priority: Priority, + window: &Window, + f: AsyncFn, + ) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncWindowContext) -> R + 'static, + { + let view = self.weak_entity(); + window.spawn_with_priority(priority, self, async move |cx| f(view, cx).await) + } + + /// Register a callback to be invoked when the given global state changes. + pub fn observe_global_in( + &mut self, + window: &Window, + mut f: impl FnMut(&mut T, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let window_handle = window.handle; + let view = self.weak_entity(); + let (subscription, activate) = self.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + // If the entity has been dropped, remove this observer. + if view.upgrade().is_none() { + return false; + } + // If the window is unavailable (e.g. temporarily taken during a + // nested update, or already closed), skip this notification but + // keep the observer alive so it can fire on future changes. + let Ok(entity_alive) = window_handle.update(cx, |_, window, cx| { + view.update(cx, |view, cx| f(view, window, cx)).is_ok() + }) else { + return true; + }; + entity_alive + }), + ); + self.defer(move |_| activate()); + subscription + } + + /// Register a callback to be invoked when the given Action type is dispatched to the window. + pub fn on_action( + &mut self, + action_type: TypeId, + window: &mut Window, + listener: impl Fn(&mut T, &dyn Any, DispatchPhase, &mut Window, &mut Context) + 'static, + ) { + let handle = self.weak_entity(); + window.on_action(action_type, move |action, phase, window, cx| { + handle + .update(cx, |view, cx| { + listener(view, action, phase, window, cx); + }) + .ok(); + }); + } + + /// Move focus to the current view, assuming it implements [`Focusable`]. + pub fn focus_self(&mut self, window: &mut Window) + where + T: Focusable, + { + let view = self.entity(); + window.defer(self, move |window, cx| { + view.read(cx).focus_handle(cx).focus(window, cx) + }) + } +} + +impl Context<'_, T> { + /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. + pub fn emit(&mut self, event: Evt) + where + T: EventEmitter, + Evt: 'static, + { + let event = self + .event_arena + .alloc(|| event) + .map(|it| it as &mut dyn Any); + self.app.pending_effects.push_back(Effect::Emit { + emitter: self.entity_state.entity_id, + event_type: TypeId::of::(), + event, + }); + } +} + +impl AppContext for Context<'_, T> { + #[inline] + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> U) -> Entity { + self.app.new(build_entity) + } + + #[inline] + fn reserve_entity(&mut self) -> Reservation { + self.app.reserve_entity() + } + + #[inline] + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> U, + ) -> Entity { + self.app.insert_entity(reservation, build_entity) + } + + #[inline] + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut U, &mut Context) -> R, + ) -> R { + self.app.update_entity(handle, update) + } + + #[inline] + fn as_mut<'a, E>(&'a mut self, handle: &Entity) -> super::GpuiBorrow<'a, E> + where + E: 'static, + { + self.app.as_mut(handle) + } + + #[inline] + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&U, &App) -> R) -> R + where + U: 'static, + { + self.app.read_entity(handle, read) + } + + #[inline] + fn update_window(&mut self, window: AnyWindowHandle, update: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> R, + { + self.app.update_window(window, update) + } + + #[inline] + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + self.app.with_window(entity_id, f) + } + + #[inline] + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + U: 'static, + { + self.app.read_window(window, read) + } + + #[inline] + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.app.background_executor.spawn(future) + } + + #[inline] + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + self.app.read_global(callback) + } +} + +impl Borrow for Context<'_, T> { + fn borrow(&self) -> &App { + self.app + } +} + +impl BorrowMut for Context<'_, T> { + fn borrow_mut(&mut self) -> &mut App { + self.app + } +} + +#[inline(never)] +fn invoke_observer( + observer: &WeakEntity, + observed: Entity, + cx: &mut App, + on_notify: &mut dyn FnMut(&mut T, Entity, &mut Context), +) -> bool { + if let Some(observer) = observer.upgrade() { + observer.update(cx, |observer, cx| on_notify(observer, observed, cx)); + true + } else { + false + } +} + +#[inline(never)] +fn invoke_subscriber( + subscriber: &WeakEntity, + emitter: Entity, + event: &Event, + cx: &mut App, + on_event: &mut dyn FnMut(&mut T, Entity, &Event, &mut Context), +) -> bool { + if let Some(subscriber) = subscriber.upgrade() { + subscriber.update(cx, |subscriber, cx| { + on_event(subscriber, emitter, event, cx) + }); + true + } else { + false + } +} + +#[inline(never)] +fn invoke_observer_in( + observer: &WeakEntity, + observed: &WeakEntity, + cx: &mut App, + on_notify: &mut dyn FnMut(&mut T, Entity, &mut Window, &mut Context), +) -> bool { + let Some((observer, observed)) = observer.upgrade().zip(observed.upgrade()) else { + return false; + }; + cx.with_window(observer.entity_id(), |window, cx| { + observer.update(cx, |observer, cx| { + on_notify(observer, observed, window, cx); + }); + }); + true +} + +#[inline(never)] +fn invoke_subscriber_in( + subscriber: &WeakEntity, + emitter: &WeakEntity, + event: &dyn Any, + cx: &mut App, + on_event: &mut dyn FnMut(&mut T, &Entity, &Event, &mut Window, &mut Context), +) -> bool { + let Some((subscriber, emitter)) = subscriber.upgrade().zip(emitter.upgrade()) else { + return false; + }; + let event = event.downcast_ref().expect("invalid event type"); + cx.with_window(subscriber.entity_id(), |window, cx| { + subscriber.update(cx, |subscriber, cx| { + on_event(subscriber, &emitter, event, window, cx); + }); + }); + true +} + +#[inline(never)] +fn invoke_listener( + view: &WeakEntity, + window: &mut Window, + cx: &mut App, + listener: &dyn Fn(&mut T, &mut Window, &mut Context), +) { + view.update(cx, |view, cx| listener(view, window, cx)).ok(); +} diff --git a/vendor/gpui-pre/src/app/entity_map.rs b/vendor/gpui-pre/src/app/entity_map.rs new file mode 100644 index 0000000..38ba760 --- /dev/null +++ b/vendor/gpui-pre/src/app/entity_map.rs @@ -0,0 +1,1308 @@ +use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed}; +use anyhow::{Context as _, Result}; +use collections::FxHashSet; +use derive_more::{Deref, DerefMut}; +use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use slotmap::{KeyData, SecondaryMap, SlotMap}; +use std::{ + any::{Any, TypeId, type_name}, + cell::RefCell, + cmp::Ordering, + fmt::{self, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + num::NonZeroU64, + sync::{ + Arc, Weak, + atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst}, + }, + thread::panicking, +}; + +use super::Context; +use crate::util::atomic_incr_if_not_zero; +#[cfg(any(test, gpui_leak_detection))] +use collections::HashMap; + +slotmap::new_key_type! { + /// A unique identifier for a entity across the application. + pub struct EntityId; +} + +impl From for EntityId { + fn from(value: u64) -> Self { + Self(KeyData::from_ffi(value)) + } +} + +impl EntityId { + /// Converts this entity id to a [NonZeroU64] + pub fn as_non_zero_u64(self) -> NonZeroU64 { + NonZeroU64::new(self.0.as_ffi()).unwrap() + } + + /// Converts this entity id to a [u64] + pub fn as_u64(self) -> u64 { + self.0.as_ffi() + } +} + +impl Display for EntityId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_u64()) + } +} + +pub(crate) struct EntityMap { + entities: SecondaryMap>, + pub accessed_entities: RefCell>, + ref_counts: Arc>, +} + +#[doc(hidden)] +pub(crate) struct EntityRefCounts { + counts: SlotMap, + dropped_entity_ids: Vec, + #[cfg(any(test, gpui_leak_detection))] + leak_detector: LeakDetector, +} + +pub(super) struct LeaseInner { + pub(super) entity: Option>, +} + +impl EntityMap { + pub fn new() -> Self { + Self { + entities: SecondaryMap::new(), + accessed_entities: RefCell::new(FxHashSet::default()), + ref_counts: Arc::new(RwLock::new(EntityRefCounts { + counts: SlotMap::with_key(), + dropped_entity_ids: Vec::new(), + #[cfg(any(test, gpui_leak_detection))] + leak_detector: LeakDetector { + next_handle_id: 0, + entity_handles: HashMap::default(), + }, + })), + } + } + + #[doc(hidden)] + pub fn ref_counts_drop_handle(&self) -> Arc> { + self.ref_counts.clone() + } + + /// Captures a snapshot of all entities that currently have alive handles. + /// + /// The returned [`LeakDetectorSnapshot`] can later be passed to + /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no + /// entities created after the snapshot are still alive. + #[cfg(any(test, gpui_leak_detection))] + pub fn leak_detector_snapshot(&self) -> LeakDetectorSnapshot { + self.ref_counts.read().leak_detector.snapshot() + } + + /// Asserts that no entities created after `snapshot` still have alive handles. + /// + /// See [`LeakDetector::assert_no_new_leaks`] for details. + #[cfg(any(test, gpui_leak_detection))] + pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { + self.ref_counts + .read() + .leak_detector + .assert_no_new_leaks(snapshot) + } + + /// Reserve a slot for an entity, which you can subsequently use with `insert`. + pub fn reserve(&self) -> Slot { + let id = self.ref_counts.write().counts.insert(1.into()); + Slot(Entity::new(id, Arc::downgrade(&self.ref_counts))) + } + + /// Insert an entity into a slot obtained by calling `reserve`. + pub fn insert(&mut self, slot: Slot, entity: T) -> Entity + where + T: 'static, + { + let mut accessed_entities = self.accessed_entities.get_mut(); + accessed_entities.insert(slot.entity_id); + + let handle = slot.0; + self.entities.insert(handle.entity_id, Box::new(entity)); + handle + } + + /// Move an entity to the stack. + #[track_caller] + pub fn lease(&mut self, pointer: &Entity) -> Lease { + Lease { + inner: self.lease_erased(pointer, type_name::()), + id: pointer.entity_id, + entity_type: PhantomData, + } + } + + /// Returns an entity after moving it to the stack. + pub fn end_lease(&mut self, lease: Lease) { + self.end_lease_erased(lease.id, lease.inner); + } + + #[inline(always)] + pub fn read(&self, entity: &Entity) -> &T { + self.assert_valid_context(entity); + self.read_inner(entity.entity_id) + .and_then(|entity| entity.downcast_ref()) + .unwrap_or_else(|| double_lease_panic("read", type_name::())) + } + + #[track_caller] + pub(super) fn lease_erased(&mut self, pointer: &AnyEntity, entity_type: &str) -> LeaseInner { + self.assert_valid_context(pointer); + let entity = Some( + self.lease_inner(pointer.entity_id) + .unwrap_or_else(|| double_lease_panic("update", entity_type)), + ); + LeaseInner { entity } + } + + pub(super) fn end_lease_erased(&mut self, entity_id: EntityId, mut lease: LeaseInner) { + self.end_lease_inner(entity_id, lease.entity.take().unwrap()); + } + + fn assert_valid_context(&self, entity: &AnyEntity) { + debug_assert!( + Weak::ptr_eq(&entity.entity_map, &Arc::downgrade(&self.ref_counts)), + "used a entity with the wrong context" + ); + } + + pub fn extend_accessed(&mut self, entities: &FxHashSet) { + self.accessed_entities + .get_mut() + .extend(entities.iter().copied()); + } + + pub fn clear_accessed(&mut self) { + self.accessed_entities.get_mut().clear(); + } + + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { + let mut ref_counts = &mut *self.ref_counts.write(); + let dropped_entity_ids = ref_counts.dropped_entity_ids.drain(..); + let mut accessed_entities = self.accessed_entities.get_mut(); + + dropped_entity_ids + .filter_map(|entity_id| { + let count = ref_counts.counts.remove(entity_id).unwrap(); + debug_assert_eq!( + count.load(SeqCst), + 0, + "dropped an entity that was referenced" + ); + accessed_entities.remove(&entity_id); + // If the EntityId was allocated with `Context::reserve`, + // the entity may not have been inserted. + Some((entity_id, self.entities.remove(entity_id)?)) + }) + .collect() + } + + #[inline(never)] + fn read_inner(&self, entity_id: EntityId) -> Option<&dyn Any> { + let mut accessed_entities = self.accessed_entities.borrow_mut(); + accessed_entities.insert(entity_id); + self.entities.get(entity_id).map(Box::as_ref) + } + + #[inline(never)] + fn lease_inner(&mut self, entity_id: EntityId) -> Option> { + self.accessed_entities.get_mut().insert(entity_id); + self.entities.remove(entity_id) + } + + #[inline(never)] + fn end_lease_inner(&mut self, entity_id: EntityId, entity: Box) { + self.entities.insert(entity_id, entity); + } +} + +#[track_caller] +fn double_lease_panic(operation: &str, entity_type: &str) -> ! { + panic!("cannot {operation} {entity_type} while it is already being updated") +} + +pub(crate) struct Lease { + pub id: EntityId, + inner: LeaseInner, + entity_type: PhantomData, +} + +impl core::ops::Deref for Lease { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.entity.as_ref().unwrap().downcast_ref().unwrap() + } +} + +impl core::ops::DerefMut for Lease { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.entity.as_mut().unwrap().downcast_mut().unwrap() + } +} + +impl Drop for LeaseInner { + fn drop(&mut self) { + if self.entity.is_some() && !panicking() { + panic!("Leases must be ended with EntityMap::end_lease") + } + } +} + +#[derive(Deref, DerefMut)] +pub(crate) struct Slot(Entity); + +/// A dynamically typed reference to a entity, which can be downcast into a `Entity`. +pub struct AnyEntity { + pub(crate) entity_id: EntityId, + pub(crate) entity_type: TypeId, + entity_map: Weak>, + #[cfg(any(test, gpui_leak_detection))] + handle_id: HandleId, +} + +impl AnyEntity { + fn new( + id: EntityId, + entity_type: TypeId, + entity_map: Weak>, + #[cfg(any(test, gpui_leak_detection))] type_name: &'static str, + ) -> Self { + Self { + entity_id: id, + entity_type, + #[cfg(any(test, gpui_leak_detection))] + handle_id: entity_map + .clone() + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(id, Some(type_name)), + entity_map, + } + } + + /// Returns the id associated with this entity. + #[inline] + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Returns the [TypeId] associated with this entity. + #[inline] + pub fn entity_type(&self) -> TypeId { + self.entity_type + } + + /// Converts this entity handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> AnyWeakEntity { + AnyWeakEntity { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_ref_counts: self.entity_map.clone(), + } + } + + /// Converts this entity handle into a strongly-typed entity handle of the given type. + /// If this entity handle is not of the specified type, returns itself as an error variant. + pub fn downcast(self) -> Result, AnyEntity> { + if TypeId::of::() == self.entity_type { + Ok(Entity { + any_entity: self, + entity_type: PhantomData, + }) + } else { + Err(self) + } + } +} + +impl Clone for AnyEntity { + fn clone(&self) -> Self { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a entity"); + let prev_count = count.fetch_add(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a entity."); + } + + Self { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_map.clone(), + #[cfg(any(test, gpui_leak_detection))] + handle_id: self + .entity_map + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id, None), + } + } +} + +impl Drop for AnyEntity { + fn drop(&mut self) { + if let Some(entity_map) = self.entity_map.upgrade() { + let entity_map = entity_map.upgradable_read(); + let count = entity_map + .counts + .get(self.entity_id) + .expect("detected over-release of a handle."); + let prev_count = count.fetch_sub(1, SeqCst); + assert_ne!(prev_count, 0, "Detected over-release of a entity."); + if prev_count == 1 { + // We were the last reference to this entity, so we can remove it. + let mut entity_map = RwLockUpgradableReadGuard::upgrade(entity_map); + entity_map.dropped_entity_ids.push(self.entity_id); + } + } + + #[cfg(any(test, gpui_leak_detection))] + if let Some(entity_map) = self.entity_map.upgrade() { + entity_map + .write() + .leak_detector + .handle_released(self.entity_id, self.handle_id) + } + } +} + +impl From> for AnyEntity { + #[inline] + fn from(entity: Entity) -> Self { + entity.any_entity + } +} + +impl Hash for AnyEntity { + #[inline] + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyEntity { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyEntity {} + +impl Ord for AnyEntity { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id.cmp(&other.entity_id) + } +} + +impl PartialOrd for AnyEntity { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::fmt::Debug for AnyEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyEntity") + .field("entity_id", &self.entity_id.as_u64()) + .finish() + } +} + +/// A strong, well-typed reference to a struct which is managed +/// by GPUI +#[derive(Deref, DerefMut)] +pub struct Entity { + #[deref] + #[deref_mut] + pub(crate) any_entity: AnyEntity, + pub(crate) entity_type: PhantomData T>, +} + +impl Sealed for Entity {} + +impl Entity { + #[inline] + fn new(id: EntityId, entity_map: Weak>) -> Self + where + T: 'static, + { + Self { + any_entity: AnyEntity::new( + id, + TypeId::of::(), + entity_map, + #[cfg(any(test, gpui_leak_detection))] + std::any::type_name::(), + ), + entity_type: PhantomData, + } + } + + /// Get the entity ID associated with this entity + #[inline] + pub fn entity_id(&self) -> EntityId { + self.any_entity.entity_id + } + + /// Downgrade this entity pointer to a non-retaining weak pointer + #[inline] + pub fn downgrade(&self) -> WeakEntity { + WeakEntity { + any_entity: self.any_entity.downgrade(), + entity_type: self.entity_type, + } + } + + /// Convert this into a dynamically typed entity. + #[inline] + pub fn into_any(self) -> AnyEntity { + self.any_entity + } + + /// Grab a reference to this entity from the context. + #[inline] + pub fn read<'a>(&self, cx: &'a App) -> &'a T { + cx.entities.read(self) + } + + /// Read the entity referenced by this handle with the given function. + #[inline] + pub fn read_with(&self, cx: &C, f: impl FnOnce(&T, &App) -> R) -> R { + cx.read_entity(self, f) + } + + /// Updates the entity referenced by this handle with the given function. + #[inline] + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + cx.update_entity(self, update) + } + + /// Updates the entity referenced by this handle with the given function. + #[inline] + pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> GpuiBorrow<'a, T> { + cx.as_mut(self) + } + + /// Updates the entity referenced by this handle with the given function. + pub fn write(&self, cx: &mut C, value: T) { + self.update(cx, |entity, cx| { + *entity = value; + cx.notify(); + }) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists, within a visual context that has a window. + /// Returns an error if the window has been closed. + #[inline] + pub fn update_in( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> C::Result { + cx.update_window_entity(self, update) + } +} + +impl Clone for Entity { + #[inline] + fn clone(&self) -> Self { + Self { + any_entity: self.any_entity.clone(), + entity_type: self.entity_type, + } + } +} + +impl std::fmt::Debug for Entity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Entity") + .field("entity_id", &self.any_entity.entity_id) + .field("entity_type", &type_name::()) + .finish() + } +} + +impl Hash for Entity { + #[inline] + fn hash(&self, state: &mut H) { + self.any_entity.hash(state); + } +} + +impl PartialEq for Entity { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.any_entity == other.any_entity + } +} + +impl Eq for Entity {} + +impl PartialEq> for Entity { + #[inline] + fn eq(&self, other: &WeakEntity) -> bool { + self.any_entity.entity_id() == other.entity_id() + } +} + +impl Ord for Entity { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.entity_id().cmp(&other.entity_id()) + } +} + +impl PartialOrd for Entity { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A type erased, weak reference to a entity. +#[derive(Clone)] +pub struct AnyWeakEntity { + pub(crate) entity_id: EntityId, + entity_type: TypeId, + entity_ref_counts: Weak>, +} + +impl AnyWeakEntity { + /// Get the entity ID associated with this weak reference. + #[inline] + pub fn entity_id(&self) -> EntityId { + self.entity_id + } + + /// Check if this weak handle can be upgraded, or if the entity has already been dropped + pub fn is_upgradable(&self) -> bool { + let ref_count = self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .unwrap_or(0); + ref_count > 0 + } + + /// Upgrade this weak entity reference to a strong reference. + pub fn upgrade(&self) -> Option { + let ref_counts = &self.entity_ref_counts.upgrade()?; + let ref_counts = ref_counts.read(); + let ref_count = ref_counts.counts.get(self.entity_id)?; + + if atomic_incr_if_not_zero(ref_count) == 0 { + // entity_id is in dropped_entity_ids + return None; + } + drop(ref_counts); + + Some(AnyEntity { + entity_id: self.entity_id, + entity_type: self.entity_type, + entity_map: self.entity_ref_counts.clone(), + #[cfg(any(test, gpui_leak_detection))] + handle_id: self + .entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .handle_created(self.entity_id, None), + }) + } + + /// Asserts that the entity referenced by this weak handle has been fully released. + /// + /// # Example + /// + /// ```ignore + /// let entity = cx.new(|_| MyEntity::new()); + /// let weak = entity.downgrade(); + /// drop(entity); + /// + /// // Verify the entity was released + /// weak.assert_released(); + /// ``` + /// + /// # Debugging Leaks + /// + /// With leak detection enabled (build with `GPUI_LEAK_DETECTION=1`), a failure + /// lists each leaked handle. Also set the `LEAK_BACKTRACE` environment variable + /// to see where they were allocated: + /// + /// ```bash + /// GPUI_LEAK_DETECTION=1 LEAK_BACKTRACE=1 cargo test my_test + /// ``` + /// + /// # Panics + /// + /// - Panics if any strong handles to the entity are still alive. + /// - Panics if the entity was recently dropped but cleanup hasn't completed yet + /// (resources are retained until the end of the effect cycle). + #[cfg(any(test, feature = "test-support", gpui_leak_detection))] + pub fn assert_released(&self) { + #[cfg(any(test, gpui_leak_detection))] + self.entity_ref_counts + .upgrade() + .unwrap() + .write() + .leak_detector + .assert_released(self.entity_id); + + if self + .entity_ref_counts + .upgrade() + .and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst))) + .is_some() + { + panic!( + "entity is still alive, or was recently dropped and its resources are retained \ + until the end of the effect cycle. Build with GPUI_LEAK_DETECTION=1 to list \ + its live handles." + ) + } + } + + /// Creates a weak entity that can never be upgraded. + pub fn new_invalid() -> Self { + /// To hold the invariant that all ids are unique, and considering that slotmap + /// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these + /// two will never conflict (u64 is way too large). + static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX); + let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst); + + Self { + // Safety: + // Docs say this is safe but can be unspecified if slotmap changes the representation + // after `1.0.7`, that said, providing a valid entity_id here is not necessary as long + // as we guarantee that `entity_id` is never used if `entity_ref_counts` equals + // to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that + // actually needs to be hold true. + // + // And there is no sane reason to read an entity slot if `entity_ref_counts` can't be + // read in the first place, so we're good! + entity_id: entity_id.into(), + entity_type: TypeId::of::<()>(), + entity_ref_counts: Weak::new(), + } + } +} + +impl std::fmt::Debug for AnyWeakEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(type_name::()) + .field("entity_id", &self.entity_id) + .field("entity_type", &self.entity_type) + .finish() + } +} + +impl From> for AnyWeakEntity { + #[inline] + fn from(entity: WeakEntity) -> Self { + entity.any_entity + } +} + +impl Hash for AnyWeakEntity { + #[inline] + fn hash(&self, state: &mut H) { + self.entity_id.hash(state); + } +} + +impl PartialEq for AnyWeakEntity { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.entity_id == other.entity_id + } +} + +impl Eq for AnyWeakEntity {} + +impl Ord for AnyWeakEntity { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id.cmp(&other.entity_id) + } +} + +impl PartialOrd for AnyWeakEntity { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A weak reference to a entity of the given type. +#[derive(Deref, DerefMut)] +pub struct WeakEntity { + #[deref] + #[deref_mut] + any_entity: AnyWeakEntity, + entity_type: PhantomData T>, +} + +impl std::fmt::Debug for WeakEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(type_name::()) + .field("entity_id", &self.any_entity.entity_id) + .field("entity_type", &type_name::()) + .finish() + } +} + +impl Clone for WeakEntity { + fn clone(&self) -> Self { + Self { + any_entity: self.any_entity.clone(), + entity_type: self.entity_type, + } + } +} + +impl WeakEntity { + /// Upgrade this weak entity reference into a strong entity reference + pub fn upgrade(&self) -> Option> { + Some(Entity { + any_entity: self.any_entity.upgrade()?, + entity_type: self.entity_type, + }) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + #[inline(always)] + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> Result + where + C: AppContext, + { + let entity = self.upgrade().context("entity released")?; + Ok(cx.update_entity(&entity, update)) + } + + /// Updates the entity referenced by this handle with the given function if + /// the referenced entity still exists, within a visual context that has a window. + /// Returns an error if the entity has been released. + #[inline(always)] + pub fn update_in( + &self, + cx: &mut C, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Result + where + C: AppContext, + { + let entity = self.upgrade().context("entity released")?; + cx.with_window(entity.entity_id(), |window, app| { + entity.update(app, |entity, cx| update(entity, window, cx)) + }) + .context("entity has no current window") + } + + /// Reads the entity referenced by this handle with the given function if + /// the referenced entity still exists. Returns an error if the entity has + /// been released. + #[inline(always)] + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result + where + C: AppContext, + { + let entity = self.upgrade().context("entity released")?; + Ok(cx.read_entity(&entity, read)) + } + + /// Create a new weak entity that can never be upgraded. + #[inline] + pub fn new_invalid() -> Self { + Self { + any_entity: AnyWeakEntity::new_invalid(), + entity_type: PhantomData, + } + } +} + +impl Hash for WeakEntity { + #[inline] + fn hash(&self, state: &mut H) { + self.any_entity.hash(state); + } +} + +impl PartialEq for WeakEntity { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.any_entity == other.any_entity + } +} + +impl Eq for WeakEntity {} + +impl PartialEq> for WeakEntity { + #[inline] + fn eq(&self, other: &Entity) -> bool { + self.entity_id() == other.any_entity.entity_id() + } +} + +impl Ord for WeakEntity { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.entity_id().cmp(&other.entity_id()) + } +} + +impl PartialOrd for WeakEntity { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Controls whether backtraces are captured when entity handles are created. +/// +/// Set the `LEAK_BACKTRACE` environment variable to any non-empty value to enable +/// backtrace capture. This helps identify where leaked handles were allocated. +#[cfg(any(test, gpui_leak_detection))] +static LEAK_BACKTRACE: std::sync::LazyLock = + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); + +/// Unique identifier for a specific entity handle instance. +/// +/// This is distinct from `EntityId` - while multiple handles can point to the same +/// entity (same `EntityId`), each handle has its own unique `HandleId`. +#[cfg(any(test, gpui_leak_detection))] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub(crate) struct HandleId { + id: u64, +} + +/// Tracks entity handle allocations to detect leaks. +/// +/// The leak detector is enabled in tests and when the `leak-detection` feature is active. +/// It tracks every `Entity` and `AnyEntity` handle that is created and released, +/// allowing you to verify that all handles to an entity have been properly dropped. +/// +/// # How do leaks happen? +/// +/// Entities are reference-counted structures that can own other entities +/// allowing to form cycles. If such a strong-reference counted cycle is +/// created, all participating strong entities in this cycle will effectively +/// leak as they cannot be released anymore. +/// +/// Cycles can also happen if an entity owns a task or subscription that it +/// itself owns a strong reference to the entity again. +/// +/// # Usage +/// +/// You can use `WeakEntity::assert_released` or `AnyWeakEntity::assert_released` +/// to verify that an entity has been fully released: +/// +/// ```ignore +/// let entity = cx.new(|_| MyEntity::new()); +/// let weak = entity.downgrade(); +/// drop(entity); +/// +/// // This will panic if any handles to the entity are still alive +/// weak.assert_released(); +/// ``` +/// +/// # Debugging Leaks +/// +/// When a leak is detected, the detector will panic with information about the leaked +/// handles. To see where the leaked handles were allocated, set the `LEAK_BACKTRACE` +/// environment variable: +/// +/// ```bash +/// LEAK_BACKTRACE=1 cargo test my_test +/// ``` +/// +/// This will capture and display backtraces for each leaked handle, helping you +/// identify where leaked handles were created. +/// +/// # How It Works +/// +/// - When an entity handle is created (via `Entity::new`, `Entity::clone`, or +/// `WeakEntity::upgrade`), `handle_created` is called to register the handle. +/// - When a handle is dropped, `handle_released` removes it from tracking. +/// - `assert_released` verifies that no handles remain for a given entity. +#[cfg(any(test, gpui_leak_detection))] +pub(crate) struct LeakDetector { + next_handle_id: u64, + entity_handles: HashMap, +} + +/// A snapshot of the set of alive entities at a point in time. +/// +/// Created by [`LeakDetector::snapshot`]. Can later be passed to +/// [`LeakDetector::assert_no_new_leaks`] to verify that no new entity +/// handles remain between the snapshot and the current state. +#[cfg(any(test, feature = "test-support", gpui_leak_detection))] +#[derive(Default)] +pub struct LeakDetectorSnapshot { + #[cfg(any(test, gpui_leak_detection))] + entity_ids: collections::HashSet, +} + +#[cfg(any(test, gpui_leak_detection))] +struct EntityLeakData { + handles: HashMap>, + type_name: &'static str, +} + +#[cfg(any(test, gpui_leak_detection))] +impl LeakDetector { + /// Records that a new handle has been created for the given entity. + /// + /// Returns a unique `HandleId` that must be passed to `handle_released` when + /// the handle is dropped. If `LEAK_BACKTRACE` is set, captures a backtrace + /// at the allocation site. + #[track_caller] + pub fn handle_created( + &mut self, + entity_id: EntityId, + type_name: Option<&'static str>, + ) -> HandleId { + let id = gpui_util::post_inc(&mut self.next_handle_id); + let handle_id = HandleId { id }; + let handles = self + .entity_handles + .entry(entity_id) + .or_insert_with(|| EntityLeakData { + handles: HashMap::default(), + type_name: type_name.unwrap_or(""), + }); + handles.handles.insert( + handle_id, + LEAK_BACKTRACE.then(backtrace::Backtrace::new_unresolved), + ); + handle_id + } + + /// Records that a handle has been released (dropped). + /// + /// This removes the handle from tracking. The `handle_id` should be the same + /// one returned by `handle_created` when the handle was allocated. + pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) { + if let std::collections::hash_map::Entry::Occupied(mut data) = + self.entity_handles.entry(entity_id) + { + data.get_mut().handles.remove(&handle_id); + if data.get().handles.is_empty() { + data.remove(); + } + } + } + + /// Asserts that all handles to the given entity have been released. + /// + /// # Panics + /// + /// Panics if any handles to the entity are still alive. The panic message + /// includes backtraces for each leaked handle if `LEAK_BACKTRACE` is set, + /// otherwise it suggests setting the environment variable to get more info. + pub fn assert_released(&mut self, entity_id: EntityId) { + use std::fmt::Write as _; + + if let Some(data) = self.entity_handles.remove(&entity_id) { + let mut out = String::new(); + for (_, backtrace) in data.handles { + if let Some(mut backtrace) = backtrace { + backtrace.resolve(); + let backtrace = BacktraceFormatter(backtrace); + writeln!(out, "Leaked handle:\n{:?}", backtrace).unwrap(); + } else { + writeln!( + out, + "Leaked handle: (export LEAK_BACKTRACE to find allocation site)" + ) + .unwrap(); + } + } + panic!("Handles for {} leaked:\n{out}", data.type_name); + } + } + + /// Captures a snapshot of all entity IDs that currently have alive handles. + /// + /// The returned [`LeakDetectorSnapshot`] can later be passed to + /// [`assert_no_new_leaks`](Self::assert_no_new_leaks) to verify that no + /// entities created after the snapshot are still alive. + pub fn snapshot(&self) -> LeakDetectorSnapshot { + LeakDetectorSnapshot { + entity_ids: self.entity_handles.keys().copied().collect(), + } + } + + /// Asserts that no entities created after `snapshot` still have alive handles. + /// + /// Entities that were already tracked at the time of the snapshot are ignored, + /// even if they still have handles. Only *new* entities (those whose + /// `EntityId` was not present in the snapshot) are considered leaks. + /// + /// # Panics + /// + /// Panics if any new entity handles exist. The panic message lists every + /// leaked entity with its type name, and includes allocation-site backtraces + /// when `LEAK_BACKTRACE` is set. + pub fn assert_no_new_leaks(&self, snapshot: &LeakDetectorSnapshot) { + use std::fmt::Write as _; + + let mut out = String::new(); + for (entity_id, data) in &self.entity_handles { + if snapshot.entity_ids.contains(entity_id) { + continue; + } + for (_, backtrace) in &data.handles { + if let Some(backtrace) = backtrace { + let mut backtrace = backtrace.clone(); + backtrace.resolve(); + let backtrace = BacktraceFormatter(backtrace); + writeln!( + out, + "Leaked handle for entity {} ({entity_id:?}):\n{:?}", + data.type_name, backtrace + ) + .unwrap(); + } else { + writeln!( + out, + "Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)", + data.type_name + ) + .unwrap(); + } + } + } + + if !out.is_empty() { + panic!("New entity leaks detected since snapshot:\n{out}"); + } + } +} + +#[cfg(any(test, gpui_leak_detection))] +impl Drop for LeakDetector { + fn drop(&mut self) { + use std::fmt::Write; + + if self.entity_handles.is_empty() || std::thread::panicking() { + return; + } + + let mut out = String::new(); + for (entity_id, data) in self.entity_handles.drain() { + for (_handle, backtrace) in data.handles { + if let Some(mut backtrace) = backtrace { + backtrace.resolve(); + let backtrace = BacktraceFormatter(backtrace); + writeln!( + out, + "Leaked handle for entity {} ({entity_id:?}):\n{:?}", + data.type_name, backtrace + ) + .unwrap(); + } else { + writeln!( + out, + "Leaked handle for entity {} ({entity_id:?}): (export LEAK_BACKTRACE to find allocation site)", + data.type_name + ) + .unwrap(); + } + } + } + panic!("Exited with leaked handles:\n{out}"); + } +} + +#[cfg(any(test, gpui_leak_detection))] +struct BacktraceFormatter(backtrace::Backtrace); + +#[cfg(any(test, gpui_leak_detection))] +impl fmt::Debug for BacktraceFormatter { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + use backtrace::{BacktraceFmt, BytesOrWideString, PrintFmt}; + + let style = if fmt.alternate() { + PrintFmt::Full + } else { + PrintFmt::Short + }; + + // When printing paths we try to strip the cwd if it exists, otherwise + // we just print the path as-is. Note that we also only do this for the + // short format, because if it's full we presumably want to print + // everything. + let cwd = std::env::current_dir(); + let mut print_path = move |fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>| { + let path = path.into_path_buf(); + if style != PrintFmt::Full { + if let Ok(cwd) = &cwd { + if let Ok(suffix) = path.strip_prefix(cwd) { + return fmt::Display::fmt(&suffix.display(), fmt); + } + } + } + fmt::Display::fmt(&path.display(), fmt) + }; + + let mut f = BacktraceFmt::new(fmt, style, &mut print_path); + f.add_context()?; + let mut strip = true; + for frame in self.0.frames() { + if let [symbol, ..] = frame.symbols() + && let Some(name) = symbol.name() + && let Some(filename) = name.as_str() + { + match filename { + "test::run_test_in_process" + | "scheduler::executor::spawn_local_with_source_location::impl$1::poll > > > >,alloc::alloc::Global> > >" => { + strip = true + } + "gpui::app::entity_map::LeakDetector::handle_created" => { + strip = false; + continue; + } + "zed::main" => { + strip = true; + f.frame().backtrace_frame(frame)?; + } + _ => {} + } + } + if strip { + continue; + } + f.frame().backtrace_frame(frame)?; + } + f.finish()?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::EntityMap; + + struct TestEntity { + pub i: i32, + } + + #[test] + fn test_entity_map_slot_assignment_before_cleanup() { + // Tests that slots are not re-used before take_dropped. + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 1 }); + + let slot = entity_map.reserve::(); + entity_map.insert(slot, TestEntity { i: 2 }); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 2); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1, 2], + ); + } + + #[test] + fn test_entity_map_weak_upgrade_before_cleanup() { + // Tests that weak handles are not upgraded before take_dropped + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let handle = entity_map.insert(slot, TestEntity { i: 1 }); + let weak = handle.downgrade(); + drop(handle); + + let strong = weak.upgrade(); + assert_eq!(strong, None); + + let dropped = entity_map.take_dropped(); + assert_eq!(dropped.len(), 1); + + assert_eq!( + dropped + .into_iter() + .map(|(_, entity)| entity.downcast::().unwrap().i) + .collect::>(), + vec![1], + ); + } + + #[test] + fn test_leak_detector_snapshot_no_leaks() { + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); + + let snapshot = entity_map.leak_detector_snapshot(); + + let slot = entity_map.reserve::(); + let temporary = entity_map.insert(slot, TestEntity { i: 2 }); + drop(temporary); + + entity_map.assert_no_new_leaks(&snapshot); + + drop(pre_existing); + } + + #[test] + #[should_panic(expected = "New entity leaks detected since snapshot")] + fn test_leak_detector_snapshot_detects_new_leak() { + let mut entity_map = EntityMap::new(); + + let slot = entity_map.reserve::(); + let pre_existing = entity_map.insert(slot, TestEntity { i: 1 }); + + let snapshot = entity_map.leak_detector_snapshot(); + + let slot = entity_map.reserve::(); + let leaked = entity_map.insert(slot, TestEntity { i: 2 }); + + // `leaked` is still alive, so this should panic. + entity_map.assert_no_new_leaks(&snapshot); + + drop(pre_existing); + drop(leaked); + } +} diff --git a/vendor/gpui-pre/src/app/headless_app_context.rs b/vendor/gpui-pre/src/app/headless_app_context.rs new file mode 100644 index 0000000..6ac4bfe --- /dev/null +++ b/vendor/gpui-pre/src/app/headless_app_context.rs @@ -0,0 +1,284 @@ +//! Cross-platform headless app context for tests that need real text shaping. +//! +//! This replaces the macOS-only `HeadlessMetalAppContext` with a platform-neutral +//! implementation backed by `TestPlatform`. Tests supply a real `PlatformTextSystem` +//! (e.g. `DirectWriteTextSystem` on Windows, `MacTextSystem` on macOS) to get +//! accurate glyph measurements while keeping everything else deterministic. +//! +//! Optionally, a renderer factory can be provided to enable real GPU rendering +//! and screenshot capture via [`HeadlessAppContext::capture_screenshot`]. + +use crate::{ + AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, Bounds, + Context, Entity, EntityId, ForegroundExecutor, Global, Pixels, PlatformHeadlessRenderer, + PlatformTextSystem, Render, Reservation, Size, Task, TestDispatcher, TestPlatform, TextSystem, + Window, WindowBounds, WindowHandle, WindowOptions, + app::{GpuiBorrow, GpuiMode}, +}; +use anyhow::Result; +use image::RgbaImage; +use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; + +/// A cross-platform headless app context for tests that need real text shaping. +/// +/// Unlike the old `HeadlessMetalAppContext`, this works on any platform. It uses +/// `TestPlatform` for deterministic scheduling and accepts a pluggable +/// `PlatformTextSystem` so tests get real glyph measurements. +/// +/// # Usage +/// +/// ```ignore +/// let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new("fallback")); +/// let mut cx = HeadlessAppContext::with_platform( +/// text_system, +/// Arc::new(Assets), +/// || gpui_platform::current_headless_renderer(), +/// ); +/// ``` +pub struct HeadlessAppContext { + /// The underlying app cell. + pub app: Rc, + /// The background executor for running async tasks. + pub background_executor: BackgroundExecutor, + /// The foreground executor for running tasks on the main thread. + pub foreground_executor: ForegroundExecutor, + dispatcher: TestDispatcher, + text_system: Arc, +} + +impl HeadlessAppContext { + /// Creates a new headless app context with the given text system. + pub fn new(platform_text_system: Arc) -> Self { + Self::with_platform(platform_text_system, Arc::new(()), || Ok(None)) + } + + /// Creates a new headless app context with a custom text system and asset source. + pub fn with_asset_source( + platform_text_system: Arc, + asset_source: Arc, + ) -> Self { + Self::with_platform(platform_text_system, asset_source, || Ok(None)) + } + + /// Creates a new headless app context with the given text system, asset source, + /// and an optional renderer factory for screenshot support. + pub fn with_platform( + platform_text_system: Arc, + asset_source: Arc, + renderer_factory: impl Fn() -> Result>> + 'static, + ) -> Self { + let seed = std::env::var("SEED") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let dispatcher = TestDispatcher::new(seed); + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + + let renderer_factory: Box Result>>> = + Box::new(renderer_factory); + let platform = TestPlatform::with_platform( + background_executor.clone(), + foreground_executor.clone(), + platform_text_system.clone(), + Some(renderer_factory), + ); + + let text_system = Arc::new(TextSystem::new(platform_text_system)); + let http_client = http_client::FakeHttpClient::with_404_response(); + let app = App::new_app(platform, asset_source, http_client); + app.borrow_mut().mode = GpuiMode::test(); + + Self { + app, + background_executor, + foreground_executor, + dispatcher, + text_system, + } + } + + /// Opens a window for headless rendering. + pub fn open_window( + &mut self, + size: Size, + build_root: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> Result> { + use crate::{point, px}; + + let bounds = Bounds { + origin: point(px(0.0), px(0.0)), + size, + }; + + let mut cx = self.app.borrow_mut(); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + focus: false, + show: false, + ..Default::default() + }, + build_root, + ) + } + + /// Runs all pending tasks until parked. + pub fn run_until_parked(&self) { + self.dispatcher.run_until_parked(); + } + + /// Advances the simulated clock. + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.advance_clock(duration); + } + + /// Enables parking mode, allowing blocking on real I/O (e.g., async asset loading). + pub fn allow_parking(&self) { + self.dispatcher.allow_parking(); + } + + /// Disables parking mode, returning to deterministic test execution. + pub fn forbid_parking(&self) { + self.dispatcher.forbid_parking(); + } + + /// Updates app state. + pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { + let mut app = self.app.borrow_mut(); + f(&mut app) + } + + /// Updates a window and calls draw to render. + pub fn update_window( + &mut self, + window: AnyWindowHandle, + f: impl FnOnce(AnyView, &mut Window, &mut App) -> R, + ) -> Result { + let mut app = self.app.borrow_mut(); + app.update_window(window, f) + } + + /// Captures a screenshot from a window. + /// + /// Requires that the context was created with a renderer factory that + /// returns `Some` via [`HeadlessAppContext::with_platform`]. + pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { + let mut app = self.app.borrow_mut(); + app.update_window(window, |_, window, _| window.render_to_image())? + } + + /// Returns the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Returns the background executor. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Returns the foreground executor. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } +} + +impl Drop for HeadlessAppContext { + fn drop(&mut self) { + // Shut down the app so windows are closed and entity handles are + // released before the LeakDetector runs. + self.app.borrow_mut().shutdown(); + } +} + +impl AppContext for HeadlessAppContext { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let mut app = self.app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> Reservation { + let mut app = self.app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let mut app = self.app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let mut app = self.app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static, + { + panic!("Cannot use as_mut with HeadlessAppContext. Call update() instead.") + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let app = self.app.borrow(); + app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let mut lock = self.app.borrow_mut(); + lock.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let app = self.app.borrow(); + app.read_global(callback) + } +} diff --git a/vendor/gpui-pre/src/app/test_app.rs b/vendor/gpui-pre/src/app/test_app.rs new file mode 100644 index 0000000..bf265fd --- /dev/null +++ b/vendor/gpui-pre/src/app/test_app.rs @@ -0,0 +1,645 @@ +//! A clean testing API for GPUI applications. +//! +//! `TestApp` provides a simpler alternative to `TestAppContext` with: +//! - Automatic effect flushing after updates +//! - Clean window creation and inspection +//! - Input simulation helpers +//! +//! # Example +//! ```ignore +//! #[test] +//! fn test_my_view() { +//! let mut app = TestApp::new(); +//! +//! let mut window = app.open_window(|window, cx| { +//! MyView::new(window, cx) +//! }); +//! +//! window.update(|view, window, cx| { +//! view.do_something(cx); +//! }); +//! +//! // Check rendered state +//! assert_eq!(window.title(), Some("Expected Title")); +//! } +//! ``` + +use crate::{ + AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext, + Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, + PlatformTextSystem, Point, Render, Size, Task, TestDispatcher, TestPlatform, TextSystem, + Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode, +}; +use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; + +/// A test application context with a clean API. +/// +/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after +/// each update and provides simpler window management. +pub struct TestApp { + app: Rc, + platform: Rc, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + #[allow(dead_code)] + dispatcher: TestDispatcher, + text_system: Arc, +} + +impl TestApp { + /// Create a new test application. + pub fn new() -> Self { + Self::with_seed(0) + } + + /// Create a new test application with a specific random seed. + pub fn with_seed(seed: u64) -> Self { + Self::build(seed, None, Arc::new(())) + } + + /// Create a new test application with a custom text system for real font shaping. + pub fn with_text_system(text_system: Arc) -> Self { + Self::build(0, Some(text_system), Arc::new(())) + } + + /// Create a new test application with a custom text system and asset source. + pub fn with_text_system_and_assets( + text_system: Arc, + asset_source: Arc, + ) -> Self { + Self::build(0, Some(text_system), asset_source) + } + + fn build( + seed: u64, + platform_text_system: Option>, + asset_source: Arc, + ) -> Self { + let dispatcher = TestDispatcher::new(seed); + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + let platform = match platform_text_system.clone() { + Some(ts) => TestPlatform::with_text_system( + background_executor.clone(), + foreground_executor.clone(), + ts, + ), + None => TestPlatform::new(background_executor.clone(), foreground_executor.clone()), + }; + let http_client = http_client::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new( + platform_text_system.unwrap_or_else(|| platform.text_system.clone()), + )); + + let app = App::new_app(platform.clone(), asset_source, http_client); + app.borrow_mut().mode = GpuiMode::test(); + + Self { + app, + platform, + background_executor, + foreground_executor, + dispatcher, + text_system, + } + } + + /// Run a closure with mutable access to the App context. + /// Automatically runs until parked after the closure completes. + pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { + let result = { + let mut app = self.app.borrow_mut(); + app.update(f) + }; + self.run_until_parked(); + result + } + + /// Run a closure with read-only access to the App context. + pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { + let app = self.app.borrow(); + f(&app) + } + + /// Create a new entity in the app. + pub fn new_entity( + &mut self, + build: impl FnOnce(&mut Context) -> T, + ) -> Entity { + self.update(|cx| cx.new(build)) + } + + /// Update an entity. + pub fn update_entity( + &mut self, + entity: &Entity, + f: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + self.update(|cx| entity.update(cx, f)) + } + + /// Read an entity. + pub fn read_entity( + &self, + entity: &Entity, + f: impl FnOnce(&T, &App) -> R, + ) -> R { + self.read(|cx| f(entity.read(cx), cx)) + } + + /// Open a test window with the given root view, using maximized bounds. + pub fn open_window( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> TestAppWindow { + let bounds = self.read(|cx| Bounds::maximized(None, cx)); + let handle = self.update(|cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_view(window, cx)), + ) + .unwrap() + }); + + TestAppWindow { + handle, + app: self.app.clone(), + platform: self.platform.clone(), + background_executor: self.background_executor.clone(), + } + } + + /// Open a test window with specific options. + pub fn open_window_with_options( + &mut self, + options: WindowOptions, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> TestAppWindow { + let handle = self.update(|cx| { + cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx))) + .unwrap() + }); + + TestAppWindow { + handle, + app: self.app.clone(), + platform: self.platform.clone(), + background_executor: self.background_executor.clone(), + } + } + + /// Run pending tasks until there's nothing left to do. + pub fn run_until_parked(&self) { + self.background_executor.run_until_parked(); + } + + /// Advance the simulated clock by the given duration. + pub fn advance_clock(&self, duration: Duration) { + self.background_executor.advance_clock(duration); + } + + /// Spawn a future on the foreground executor. + pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.to_async())) + } + + /// Spawn a future on the background executor. + pub fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + /// Get an async handle to the app. + pub fn to_async(&self) -> AsyncApp { + AsyncApp { + app: Rc::downgrade(&self.app), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Get the background executor. + pub fn background_executor(&self) -> &BackgroundExecutor { + &self.background_executor + } + + /// Get the foreground executor. + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Get the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Check if a global of the given type exists. + pub fn has_global(&self) -> bool { + self.read(|cx| cx.has_global::()) + } + + /// Set a global value. + pub fn set_global(&mut self, global: G) { + self.update(|cx| cx.set_global(global)); + } + + /// Read a global value. + pub fn read_global(&self, f: impl FnOnce(&G, &App) -> R) -> R { + self.read(|cx| f(cx.global(), cx)) + } + + /// Update a global value. + pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R { + self.update(|cx| cx.update_global(f)) + } + + // Platform simulation methods + + /// Write text to the simulated clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item); + } + + /// Read from the simulated clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Get URLs that have been opened via `cx.open_url()`. + pub fn opened_url(&self) -> Option { + self.platform.opened_url.borrow().clone() + } + + /// Check if a file path prompt is pending. + pub fn did_prompt_for_new_path(&self) -> bool { + self.platform.did_prompt_for_new_path() + } + + /// Simulate answering a path selection dialog. + pub fn simulate_new_path_selection( + &self, + select: impl FnOnce(&std::path::Path) -> Option, + ) { + self.platform.simulate_new_path_selection(select); + } + + /// Check if a prompt dialog is pending. + pub fn has_pending_prompt(&self) -> bool { + self.platform.has_pending_prompt() + } + + /// Simulate answering a prompt dialog. + pub fn simulate_prompt_answer(&self, button: &str) { + self.platform.simulate_prompt_answer(button); + } + + /// Get all open windows. + pub fn windows(&self) -> Vec { + self.read(|cx| cx.windows()) + } +} + +impl Default for TestApp { + fn default() -> Self { + Self::new() + } +} + +/// A test window with inspection and simulation capabilities. +pub struct TestAppWindow { + handle: WindowHandle, + app: Rc, + platform: Rc, + background_executor: BackgroundExecutor, +} + +impl TestAppWindow { + /// Get the window handle. + pub fn handle(&self) -> WindowHandle { + self.handle + } + + /// Get the root view entity. + pub fn root(&self) -> Entity { + let mut app = self.app.borrow_mut(); + let any_handle: AnyWindowHandle = self.handle.into(); + app.update_window(any_handle, |root_view, _, _| { + root_view.downcast::().expect("root view type mismatch") + }) + .expect("window not found") + } + + /// Update the root view. + pub fn update(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context) -> R) -> R { + let result = { + let mut app = self.app.borrow_mut(); + let any_handle: AnyWindowHandle = self.handle.into(); + app.update_window(any_handle, |root_view, window, cx| { + let view = root_view.downcast::().expect("root view type mismatch"); + view.update(cx, |view, cx| f(view, window, cx)) + }) + .expect("window not found") + }; + self.background_executor.run_until_parked(); + result + } + + /// Read the root view. + pub fn read(&self, f: impl FnOnce(&V, &App) -> R) -> R { + let app = self.app.borrow(); + let view = self + .app + .borrow() + .windows + .get(self.handle.window_id()) + .and_then(|w| w.as_ref()) + .and_then(|w| w.root.clone()) + .and_then(|r| r.downcast::().ok()) + .expect("window or root view not found"); + f(view.read(&app), &app) + } + + /// Get the window title. + pub fn title(&self) -> Option { + let app = self.app.borrow(); + app.read_window(&self.handle, |_, _cx| { + // TODO: expose title through Window API + None + }) + .unwrap() + } + + /// Simulate a keystroke. + pub fn simulate_keystroke(&mut self, keystroke: &str) { + let keystroke = Keystroke::parse(keystroke).unwrap(); + { + let mut app = self.app.borrow_mut(); + let any_handle: AnyWindowHandle = self.handle.into(); + app.update_window(any_handle, |_, window, cx| { + window.dispatch_keystroke(keystroke, cx); + }) + .unwrap(); + } + self.background_executor.run_until_parked(); + } + + /// Simulate multiple keystrokes (space-separated). + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + for keystroke in keystrokes.split(' ') { + self.simulate_keystroke(keystroke); + } + } + + /// Simulate typing text. + pub fn simulate_input(&mut self, input: &str) { + for char in input.chars() { + self.simulate_keystroke(&char.to_string()); + } + } + + /// Simulate a mouse move. + pub fn simulate_mouse_move(&mut self, position: Point) { + self.simulate_event(MouseMoveEvent { + position, + modifiers: Default::default(), + pressed_button: None, + }); + } + + /// Simulate a mouse down event. + pub fn simulate_mouse_down(&mut self, position: Point, button: MouseButton) { + self.simulate_event(MouseDownEvent { + position, + button, + modifiers: Default::default(), + click_count: 1, + first_mouse: false, + }); + } + + /// Simulate a mouse up event. + pub fn simulate_mouse_up(&mut self, position: Point, button: MouseButton) { + self.simulate_event(MouseUpEvent { + position, + button, + modifiers: Default::default(), + click_count: 1, + }); + } + + /// Simulate a click at the given position. + pub fn simulate_click(&mut self, position: Point, button: MouseButton) { + self.simulate_mouse_down(position, button); + self.simulate_mouse_up(position, button); + } + + /// Simulate a scroll event. + pub fn simulate_scroll(&mut self, position: Point, delta: Point) { + self.simulate_event(crate::ScrollWheelEvent { + position, + delta: crate::ScrollDelta::Pixels(delta), + modifiers: Default::default(), + touch_phase: crate::TouchPhase::Moved, + }); + } + + /// Simulate an input event. + pub fn simulate_event(&mut self, event: E) { + let platform_input = event.to_platform_input(); + { + let mut app = self.app.borrow_mut(); + let any_handle: AnyWindowHandle = self.handle.into(); + app.update_window(any_handle, |_, window, cx| { + window.dispatch_event(platform_input, cx); + }) + .unwrap(); + } + self.background_executor.run_until_parked(); + } + + /// Simulate resizing the window. + pub fn simulate_resize(&mut self, size: Size) { + let window_id = self.handle.window_id(); + let mut app = self.app.borrow_mut(); + if let Some(Some(window)) = app.windows.get_mut(window_id) { + if let Some(test_window) = window.platform_window.as_test() { + test_window.simulate_resize(size); + } + } + drop(app); + self.background_executor.run_until_parked(); + } + + /// Simulate the window moving to a display with a different scale factor. + pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) { + let window_id = self.handle.window_id(); + let test_window = { + let mut app = self.app.borrow_mut(); + app.windows + .get_mut(window_id) + .and_then(|window| window.as_mut()) + .and_then(|window| window.platform_window.as_test()) + .cloned() + }; + // The resize callback needs to borrow the app again synchronously. + if let Some(mut test_window) = test_window { + test_window.simulate_scale_factor_change(scale_factor); + } + self.background_executor.run_until_parked(); + } + + /// Force a redraw of the window. + pub fn draw(&mut self) { + let mut app = self.app.borrow_mut(); + let any_handle: AnyWindowHandle = self.handle.into(); + app.update_window(any_handle, |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + } +} + +impl Clone for TestAppWindow { + fn clone(&self) -> Self { + Self { + handle: self.handle, + app: self.app.clone(), + platform: self.platform.clone(), + background_executor: self.background_executor.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{FocusHandle, Focusable, div, prelude::*}; + + struct Counter { + count: usize, + focus_handle: FocusHandle, + } + + impl Counter { + fn new(_window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + Self { + count: 0, + focus_handle, + } + } + + fn increment(&mut self, _cx: &mut Context) { + self.count += 1; + } + } + + impl Focusable for Counter { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } + } + + impl Render for Counter { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().child(format!("Count: {}", self.count)) + } + } + + #[test] + fn test_basic_usage() { + let mut app = TestApp::new(); + + let mut window = app.open_window(Counter::new); + + window.update(|counter, _window, cx| { + counter.increment(cx); + }); + + window.read(|counter, _| { + assert_eq!(counter.count, 1); + }); + + drop(window); + app.update(|cx| cx.shutdown()); + } + + #[test] + fn test_simulate_scale_factor_change() { + let mut app = TestApp::new(); + let mut window = app.open_window(|_, _| crate::EmptyView); + let viewport_size = window.update(|_, window, _| { + assert_eq!(window.scale_factor(), 2.0); + window.viewport_size() + }); + + window.simulate_scale_factor_change(1.0); + + window.update(|_, window, _| { + assert_eq!(window.scale_factor(), 1.0); + assert_eq!(window.viewport_size(), viewport_size); + }); + + drop(window); + app.update(|cx| cx.shutdown()); + } + + #[test] + fn test_entity_creation() { + let mut app = TestApp::new(); + + let entity = app.new_entity(|cx| Counter { + count: 42, + focus_handle: cx.focus_handle(), + }); + + app.read_entity(&entity, |counter, _| { + assert_eq!(counter.count, 42); + }); + + app.update_entity(&entity, |counter, _cx| { + counter.count += 1; + }); + + app.read_entity(&entity, |counter, _| { + assert_eq!(counter.count, 43); + }); + } + + #[test] + fn test_globals() { + let mut app = TestApp::new(); + + struct MyGlobal(String); + impl Global for MyGlobal {} + + assert!(!app.has_global::()); + + app.set_global(MyGlobal("hello".into())); + + assert!(app.has_global::()); + + app.read_global::(|global, _| { + assert_eq!(global.0, "hello"); + }); + + app.update_global::(|global, _| { + global.0 = "world".into(); + }); + + app.read_global::(|global, _| { + assert_eq!(global.0, "world"); + }); + } +} diff --git a/vendor/gpui-pre/src/app/test_context.rs b/vendor/gpui-pre/src/app/test_context.rs new file mode 100644 index 0000000..e7a8c06 --- /dev/null +++ b/vendor/gpui-pre/src/app/test_context.rs @@ -0,0 +1,1439 @@ +use crate::{ + Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace, + BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable, + Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, + Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Pixels, Platform, PlatformTextSystem, Point, Render, Result, SharedString, Size, + SystemNotification, SystemNotificationResponse, Task, TestDispatcher, TestPlatform, + TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds, + WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode, window::ElementArenaScope, +}; +use anyhow::{anyhow, bail}; +use futures::{Stream, StreamExt, channel::oneshot}; + +use std::{ + cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration, +}; + +/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides +/// an implementation of `Context` with additional methods that are useful in tests. +#[derive(Clone)] +pub struct TestAppContext { + #[doc(hidden)] + pub background_executor: BackgroundExecutor, + #[doc(hidden)] + pub foreground_executor: ForegroundExecutor, + #[doc(hidden)] + pub dispatcher: TestDispatcher, + test_platform: Rc, + text_system: Arc, + fn_name: Option<&'static str>, + on_quit: Rc>>>, + #[doc(hidden)] + pub app: Rc, +} + +impl AppContext for TestAppContext { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let mut app = self.app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> crate::Reservation { + let mut app = self.app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: crate::Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let mut app = self.app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let mut app = self.app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> super::GpuiBorrow<'a, T> + where + T: 'static, + { + panic!("Cannot use as_mut with a test app context. Try calling update() first") + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let app = self.app.borrow(); + app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let mut lock = self.app.borrow_mut(); + lock.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let app = self.app.borrow(); + app.read_global(callback) + } +} + +impl TestAppContext { + /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you. + pub fn build(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self { + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); + Self::build_with_platform( + dispatcher, + fn_name, + background_executor, + foreground_executor, + platform, + ) + } + + /// Creates a test context backed by the provided platform text system. + pub fn build_with_text_system( + dispatcher: TestDispatcher, + fn_name: Option<&'static str>, + platform_text_system: Arc, + ) -> Self { + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + let platform = TestPlatform::with_text_system( + background_executor.clone(), + foreground_executor.clone(), + platform_text_system, + ); + Self::build_with_platform( + dispatcher, + fn_name, + background_executor, + foreground_executor, + platform, + ) + } + + fn build_with_platform( + dispatcher: TestDispatcher, + fn_name: Option<&'static str>, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + platform: Rc, + ) -> Self { + let asset_source = Arc::new(()); + let http_client = http_client::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new(platform.text_system())); + + let app = App::new_app(platform.clone(), asset_source, http_client); + app.borrow_mut().mode = GpuiMode::test(); + + Self { + app, + background_executor, + foreground_executor, + dispatcher, + test_platform: platform, + text_system, + fn_name, + on_quit: Rc::new(RefCell::new(Vec::default())), + } + } + + /// Disables accessibility for subsequently created test windows. + pub fn disable_accessibility(&mut self) { + self.update(|cx| cx.accessibility_force_disabled = true); + } + + /// Skip all drawing operations for the duration of this test. + pub fn skip_drawing(&mut self) { + self.app.borrow_mut().mode = GpuiMode::Test { skip_drawing: true }; + } + + /// Create a single TestAppContext, for non-multi-client tests + pub fn single() -> Self { + let dispatcher = TestDispatcher::new(0); + Self::build(dispatcher, None) + } + + /// The name of the test function that created this `TestAppContext` + pub fn test_function_name(&self) -> Option<&'static str> { + self.fn_name + } + + /// Checks whether there have been any new path prompts received by the platform. + pub fn did_prompt_for_new_path(&self) -> bool { + self.test_platform.did_prompt_for_new_path() + } + + /// Returns the number of active idle sleep prevention tokens. + pub fn active_idle_sleep_preventions(&self) -> usize { + self.test_platform.active_idle_sleep_preventions() + } + + /// Sets the delay for subsequent idle sleep prevention acquisitions. + pub fn set_idle_sleep_prevention_delay(&self, delay: Duration) { + self.test_platform.set_idle_sleep_prevention_delay(delay); + } + + /// Makes subsequent idle sleep prevention acquisitions fail or succeed. + pub fn set_idle_sleep_prevention_fails(&self, fails: bool) { + self.test_platform.set_idle_sleep_prevention_fails(fails); + } + + /// returns a new `TestAppContext` re-using the same executors to interleave tasks. + pub fn new_app(&self) -> TestAppContext { + Self::build(self.dispatcher.clone(), self.fn_name) + } + + /// Called by the test helper to end the test. + /// public so the macro can call it. + pub fn quit(&self) { + self.on_quit.borrow_mut().drain(..).for_each(|f| f()); + self.app.borrow_mut().shutdown(); + } + + /// Register cleanup to run when the test ends. + pub fn on_quit(&mut self, f: impl FnOnce() + 'static) { + self.on_quit.borrow_mut().push(Box::new(f)); + } + + /// Schedules all windows to be redrawn on the next effect cycle. + pub fn refresh(&mut self) -> Result<()> { + let mut app = self.app.borrow_mut(); + app.refresh_windows(); + Ok(()) + } + + /// Returns an executor (for running tasks in the background) + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + /// Returns an executor (for running tasks on the main thread) + pub fn foreground_executor(&self) -> &ForegroundExecutor { + &self.foreground_executor + } + + /// Gives you an `&mut App` for the duration of the closure + pub fn update(&self, f: impl FnOnce(&mut App) -> R) -> R { + let mut cx = self.app.borrow_mut(); + cx.update(f) + } + + /// Gives you an `&App` for the duration of the closure + pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { + let cx = self.app.borrow(); + f(&cx) + } + + /// Adds a new window. The Window will always be backed by a `TestWindow` which + /// can be retrieved with `self.test_window(handle)` + pub fn add_window(&mut self, build_window: F) -> WindowHandle + where + F: FnOnce(&mut Window, &mut Context) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + + // Some tests rely on the window size matching the bounds of the test display + let bounds = Bounds::maximized(None, &cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_window(window, cx)), + ) + .unwrap() + } + + /// Opens a new window with a specific size. + /// + /// Unlike `add_window` which uses maximized bounds, this allows controlling + /// the window dimensions, which is important for layout-sensitive tests. + pub fn open_window( + &mut self, + window_size: Size, + build_window: F, + ) -> WindowHandle + where + F: FnOnce(&mut Window, &mut Context) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds { + origin: Point::default(), + size: window_size, + })), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_window(window, cx)), + ) + .unwrap() + } + + /// Adds a new window with no content. + pub fn add_empty_window(&mut self) -> &mut VisualTestContext { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &cx); + let window = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Empty), + ) + .unwrap(); + drop(cx); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + cx.run_until_parked(); + cx + } + + /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used + /// as a `Window` and `App` for the rest of the test. Typically you would shadow this context with + /// the returned one. `let (view, cx) = cx.add_window_view(...);` + pub fn add_window_view( + &mut self, + build_root_view: F, + ) -> (Entity, &mut VisualTestContext) + where + F: FnOnce(&mut Window, &mut Context) -> V, + V: 'static + Render, + { + let mut cx = self.app.borrow_mut(); + let bounds = Bounds::maximized(None, &cx); + let window = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| build_root_view(window, cx)), + ) + .unwrap(); + drop(cx); + let view = window.root(self).unwrap(); + let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + cx.run_until_parked(); + + // it might be nice to try and cleanup these at the end of each test. + (view, cx) + } + + /// returns the TextSystem + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Simulates writing to the platform clipboard + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.test_platform.write_to_clipboard(item) + } + + /// Simulates reading from the platform clipboard. + /// This will return the most recent value from `write_to_clipboard`. + pub fn read_from_clipboard(&self) -> Option { + self.test_platform.read_from_clipboard() + } + + /// Simulates choosing a File in the platform's "Open" dialog. + pub fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + self.test_platform.simulate_new_path_selection(select_path); + } + + /// Simulates responding to a `prompt_for_paths` ("Open") dialog. + pub fn simulate_path_prompt_response( + &self, + select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option>, + ) { + self.test_platform + .simulate_path_prompt_response(select_paths); + } + + /// Returns true if there's a path selection dialog pending. + pub fn did_prompt_for_paths(&self) -> bool { + self.test_platform.did_prompt_for_paths() + } + + /// Simulates clicking a button in an platform-level alert dialog. + #[track_caller] + pub fn simulate_prompt_answer(&self, button: &str) { + self.test_platform.simulate_prompt_answer(button); + } + + /// Returns true if there's an alert dialog open. + pub fn has_pending_prompt(&self) -> bool { + self.test_platform.has_pending_prompt() + } + + /// Returns true if there's an alert dialog open. + pub fn pending_prompt(&self) -> Option<(String, String)> { + self.test_platform.pending_prompt() + } + + /// All the urls that have been opened with cx.open_url() during this test. + pub fn opened_url(&self) -> Option { + self.test_platform.opened_url.borrow().clone() + } + + /// Returns the application identity configured during this test. + pub fn app_identity(&self) -> Option<(SharedString, SharedString)> { + self.test_platform.app_identity() + } + + /// Returns all system notifications shown during this test, in order. + pub fn shown_system_notifications(&self) -> Vec { + self.test_platform.shown_system_notifications() + } + + /// Returns the system notifications currently delivered by the test platform. + pub fn delivered_system_notifications(&self) -> Vec { + self.test_platform.delivered_system_notifications() + } + + /// Returns the tags of all system notifications dismissed during this test, in order. + pub fn dismissed_system_notifications(&self) -> Vec { + self.test_platform.dismissed_system_notifications() + } + + /// Simulates the user activating a system notification. + pub fn simulate_system_notification_response(&self, response: SystemNotificationResponse) { + self.test_platform + .simulate_system_notification_response(response); + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { + self.test_window(window_handle).simulate_resize(size); + } + + /// Simulates a change in whether the platform is presenting the window. + pub fn simulate_window_visibility_change( + &self, + window_handle: AnyWindowHandle, + visibility: WindowVisibility, + ) { + self.test_window(window_handle) + .simulate_visibility_change(visibility); + } + + /// Simulates visible viewport changes without resizing the window's layout area. + pub fn simulate_window_visual_viewport_change( + &self, + window_handle: AnyWindowHandle, + bounds: Bounds, + ) { + self.test_window(window_handle) + .simulate_visual_viewport_change(bounds); + } + + /// Simulates the window moving to a display with a different scale factor. + pub fn simulate_window_scale_factor_change( + &self, + window_handle: AnyWindowHandle, + scale_factor: f32, + ) { + self.test_window(window_handle) + .simulate_scale_factor_change(scale_factor); + } + + /// Returns true if there's an alert dialog open. + pub fn expect_restart(&self) -> oneshot::Receiver<(Option, Vec)> { + let (tx, rx) = futures::channel::oneshot::channel(); + self.test_platform.expect_restart.borrow_mut().replace(tx); + rx + } + + /// Causes the given sources to be returned if the application queries for screen + /// capture sources. + pub fn set_screen_capture_sources(&self, sources: Vec) { + self.test_platform.set_screen_capture_sources(sources); + } + + /// Returns all windows open in the test. + pub fn windows(&self) -> Vec { + self.app.borrow().windows() + } + + /// Run the given task on the main thread. + #[track_caller] + pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task + where + Fut: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f(self.to_async())) + } + + /// true if the given global is defined + pub fn has_global(&self) -> bool { + let app = self.app.borrow(); + app.has_global::() + } + + /// runs the given closure with a reference to the global + /// panics if `has_global` would return false. + pub fn read_global(&self, read: impl FnOnce(&G, &App) -> R) -> R { + let app = self.app.borrow(); + read(app.global(), &app) + } + + /// runs the given closure with a reference to the global (if set) + pub fn try_read_global(&self, read: impl FnOnce(&G, &App) -> R) -> Option { + let lock = self.app.borrow(); + Some(read(lock.try_global()?, &lock)) + } + + /// sets the global in this context. + pub fn set_global(&mut self, global: G) { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.set_global(global)) + } + + /// updates the global in this context. (panics if `has_global` would return false) + pub fn update_global(&mut self, update: impl FnOnce(&mut G, &mut App) -> R) -> R { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| cx.update_global(update)) + } + + /// Returns an `AsyncApp` which can be used to run tasks that expect to be on a background + /// thread on the current thread in tests. + pub fn to_async(&self) -> AsyncApp { + AsyncApp { + app: Rc::downgrade(&self.app), + background_executor: self.background_executor.clone(), + foreground_executor: self.foreground_executor.clone(), + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&self) { + self.dispatcher.run_until_parked(); + } + + /// Simulate dispatching an action to the currently focused node in the window. + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + .unwrap(); + + self.background_executor.run_until_parked() + } + + /// simulate_keystrokes takes a space-separated list of keys to type. + /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") + /// in Zed, this will run backspace on the current editor through the command palette. + /// This will also run the background executor until it's parked. + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke in keystrokes + .split(' ') + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// simulate_input takes a string of text to type. + /// cx.simulate_input("abc") + /// will type abc into your current editor + /// This will also run the background executor until it's parked. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { + self.dispatch_keystroke(window, keystroke); + } + + self.background_executor.run_until_parked() + } + + /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) + pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { + self.update_window(window, |_, window, cx| { + window.dispatch_keystroke(keystroke, cx) + }) + .unwrap(); + } + + /// Returns the `TestWindow` backing the given handle. + pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow { + self.app + .borrow_mut() + .windows + .get_mut(window.id) + .unwrap() + .as_deref_mut() + .unwrap() + .platform_window + .as_test() + .unwrap() + .clone() + } + + /// Returns a stream of notifications whenever the Entity is updated. + pub fn notifications( + &mut self, + entity: &Entity, + ) -> impl Stream + use { + let (tx, rx) = futures::channel::mpsc::unbounded(); + self.update(|cx| { + cx.observe(entity, { + let tx = tx.clone(); + move |_, _| { + let _ = tx.unbounded_send(()); + } + }) + .detach(); + cx.observe_release(entity, move |_, _| tx.close_channel()) + .detach() + }); + rx + } + + /// Returns a stream of events emitted by the given Entity. + pub fn events>( + &mut self, + entity: &Entity, + ) -> futures::channel::mpsc::UnboundedReceiver + where + Evt: 'static + Clone, + { + let (tx, rx) = futures::channel::mpsc::unbounded(); + entity + .update(self, |_, cx: &mut Context| { + cx.subscribe(entity, move |_entity, _handle, event, _cx| { + let _ = tx.unbounded_send(event.clone()); + }) + }) + .detach(); + rx + } + + /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you + /// don't need to jump in at a specific time). + pub async fn condition( + &mut self, + entity: &Entity, + mut predicate: impl FnMut(&mut T, &mut Context) -> bool, + ) { + let timer = self.executor().timer(Duration::from_secs(3)); + let mut notifications = self.notifications(entity); + + use futures::FutureExt as _; + use futures_concurrency::future::Race as _; + + ( + async { + loop { + if entity.update(self, &mut predicate) { + return Ok(()); + } + + if notifications.next().await.is_none() { + bail!("entity dropped") + } + } + }, + timer.map(|_| Err(anyhow!("condition timed out"))), + ) + .race() + .await + .unwrap(); + } + + /// Set a name for this App. + #[cfg(any(test, feature = "test-support"))] + pub fn set_name(&mut self, name: &'static str) { + self.update(|cx| cx.name = Some(name)) + } +} + +impl Entity { + /// Block until the next event is emitted by the entity, then return it. + pub fn next_event(&self, cx: &mut TestAppContext) -> impl Future + where + Event: Send + Clone + 'static, + T: EventEmitter, + { + let (tx, mut rx) = oneshot::channel(); + let mut tx = Some(tx); + let subscription = self.update(cx, |_, cx| { + cx.subscribe(self, move |_, _, event, _| { + if let Some(tx) = tx.take() { + _ = tx.send(event.clone()); + } + }) + }); + + async move { + let event = rx.await.expect("no event emitted"); + drop(subscription); + event + } + } +} + +impl Entity { + /// Returns a future that resolves when the view is next updated. + pub fn next_notification( + &self, + advance_clock_by: Duration, + cx: &TestAppContext, + ) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let subscription = cx.app.borrow_mut().observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + cx.executor().advance_clock(advance_clock_by); + + async move { + rx.recv() + .await + .expect("entity dropped while test was waiting for its next notification"); + drop(subscription); + } + } +} + +impl Entity { + /// Returns a future that resolves when the condition becomes true. + pub fn condition( + &self, + cx: &TestAppContext, + mut predicate: impl FnMut(&V, &App) -> bool, + ) -> impl Future + where + Evt: 'static, + V: EventEmitter, + { + use postage::prelude::{Sink as _, Stream as _}; + + let (tx, mut rx) = postage::mpsc::channel(1024); + + let mut cx = cx.app.borrow_mut(); + let subscriptions = ( + cx.observe(self, { + let mut tx = tx.clone(); + move |_, _| { + tx.blocking_send(()).ok(); + } + }), + cx.subscribe(self, { + let mut tx = tx; + move |_, _: &Evt, _| { + tx.blocking_send(()).ok(); + } + }), + ); + + let cx = cx.this.upgrade().unwrap(); + let handle = self.downgrade(); + + async move { + loop { + { + let cx = cx.borrow(); + let cx = &*cx; + if predicate( + handle + .upgrade() + .expect("view dropped with pending condition") + .read(cx), + cx, + ) { + break; + } + } + + rx.recv() + .await + .expect("view dropped with pending condition"); + } + drop(subscriptions); + } + } +} + +use derive_more::{Deref, DerefMut}; + +use super::{Context, Entity}; +#[derive(Deref, DerefMut, Clone)] +/// A VisualTestContext is the test-equivalent of a `Window` and `App`. It allows you to +/// run window-specific test code. It can be dereferenced to a `TextAppContext`. +pub struct VisualTestContext { + #[deref] + #[deref_mut] + /// cx is the original TestAppContext (you can more easily access this using Deref) + pub cx: TestAppContext, + window: AnyWindowHandle, +} + +impl VisualTestContext { + /// Provides a `Window` and `App` for the duration of the closure. + pub fn update(&mut self, f: impl FnOnce(&mut Window, &mut App) -> R) -> R { + self.cx + .update_window(self.window, |_, window, cx| f(window, cx)) + .unwrap() + } + + /// Creates a new VisualTestContext. You would typically shadow the passed in + /// TestAppContext with this, as this is typically more useful. + /// `let cx = VisualTestContext::from_window(window, cx);` + pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { + Self { + cx: cx.clone(), + window, + } + } + + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&self) { + self.cx.background_executor.run_until_parked(); + } + + /// Dispatch the action to the currently focused node. + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } + + /// Read the title off the window (set by `Window#set_window_title`) + pub fn window_title(&mut self) -> Option { + self.cx.test_window(self.window).0.lock().title.clone() + } + + /// Read the document path off the window (set by `Window#set_document_path`) + pub fn document_path(&mut self) -> Option { + self.cx + .test_window(self.window) + .0 + .lock() + .document_path + .clone() + } + + /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` + /// Automatically runs until parked. + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + self.cx.simulate_keystrokes(self.window, keystrokes) + } + + /// Simulate typing text `cx.simulate_input("hello")` + /// Automatically runs until parked. + pub fn simulate_input(&mut self, input: &str) { + self.cx.simulate_input(self.window, input) + } + + /// Simulate a mouse move event to the given point + pub fn simulate_mouse_move( + &mut self, + position: Point, + button: impl Into>, + modifiers: Modifiers, + ) { + self.simulate_event(MouseMoveEvent { + position, + modifiers, + pressed_button: button.into(), + }) + } + + /// Simulate a mouse down event to the given point + pub fn simulate_mouse_down( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button, + click_count: 1, + first_mouse: false, + }) + } + + /// Simulate a mouse up event to the given point + pub fn simulate_mouse_up( + &mut self, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event(MouseUpEvent { + position, + modifiers, + button, + click_count: 1, + }) + } + + /// Simulate a primary mouse click at the given point + pub fn simulate_click(&mut self, position: Point, modifiers: Modifiers) { + self.simulate_event(MouseDownEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + first_mouse: false, + }); + self.simulate_event(MouseUpEvent { + position, + modifiers, + button: MouseButton::Left, + click_count: 1, + }); + } + + /// Simulate a modifiers changed event + pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) { + self.simulate_event(ModifiersChangedEvent { + modifiers, + capslock: Capslock { on: false }, + }) + } + + /// Simulate a capslock changed event + pub fn simulate_capslock_change(&mut self, on: bool) { + self.simulate_event(ModifiersChangedEvent { + modifiers: Modifiers::none(), + capslock: Capslock { on }, + }) + } + + /// Simulates the user resizing the window to the new size. + pub fn simulate_resize(&self, size: Size) { + self.simulate_window_resize(self.window, size) + } + + /// Simulates a change in whether the platform is presenting this window. + pub fn simulate_visibility_change(&self, visibility: WindowVisibility) { + self.simulate_window_visibility_change(self.window, visibility); + } + + /// Simulates the window moving to a display with a different scale factor. + pub fn simulate_scale_factor_change(&self, scale_factor: f32) { + self.simulate_window_scale_factor_change(self.window, scale_factor) + } + + /// Returns the bounds of the element with the given debug selector. + /// + /// Returns `None` in builds without `debug_assertions`, which don't record + /// selectors; see [`crate::InteractiveElement::debug_selector`]. + pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { + self.update(|window, _| window.rendered_frame.debug_bounds.get(selector).copied()) + } + + /// Draw an element to the window. Useful for simulating events or actions + pub fn draw( + &mut self, + origin: Point, + space: impl Into>, + f: impl FnOnce(&mut Window, &mut App) -> E, + ) -> (E::RequestLayoutState, E::PrepaintState) + where + E: Element, + { + self.update(|window, cx| { + let arena_scope = ElementArenaScope::enter(&cx.element_arena); + + window.invalidator.set_phase(DrawPhase::Prepaint); + let mut element = Drawable::new(f(window, cx)); + element.layout_as_root(space.into(), window, cx); + window.with_absolute_element_offset(origin, |window| element.prepaint(window, cx)); + + window.invalidator.set_phase(DrawPhase::Paint); + let (request_layout_state, prepaint_state) = element.paint(window, cx); + + window.invalidator.set_phase(DrawPhase::None); + window.refresh(); + + drop(element); + arena_scope.exit(&cx.element_arena).clear(cx); + + (request_layout_state, prepaint_state) + }) + } + + /// Simulate an event from the platform, e.g. a ScrollWheelEvent + /// Make sure you've called [VisualTestContext::draw] first! + pub fn simulate_event(&mut self, event: E) { + self.test_window(self.window) + .simulate_input(event.to_platform_input()); + self.background_executor.run_until_parked(); + } + + /// Simulates the user blurring the window. + pub fn deactivate_window(&mut self) { + if Some(self.window) == self.test_platform.active_window() { + self.test_platform.set_active_window(None) + } + self.background_executor.run_until_parked(); + } + + /// Simulates the user closing the window. + /// Returns true if the window was closed. + pub fn simulate_close(&mut self) -> bool { + let handler = self + .cx + .update_window(self.window, |_, window, _| { + window + .platform_window + .as_test() + .unwrap() + .0 + .lock() + .should_close_handler + .take() + }) + .unwrap(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.cx + .update_window(self.window, |_, window, _| { + window.platform_window.on_should_close(handler); + }) + .unwrap(); + should_close + } else { + false + } + } + + /// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods). + /// This method internally retains the VisualTestContext until the end of the test. + pub fn into_mut(self) -> &'static mut Self { + let ptr = Box::into_raw(Box::new(self)); + // safety: on_quit will be called after the test has finished. + // the executor will ensure that all tasks related to the test have stopped. + // so there is no way for cx to be accessed after on_quit is called. + // todo: This is unsound under stacked borrows (also tree borrows probably?) + // the mutable reference invalidates `ptr` which is later used in the closure + let cx = unsafe { &mut *ptr }; + cx.on_quit(move || unsafe { + drop(Box::from_raw(ptr)); + }); + cx + } +} + +impl AppContext for VisualTestContext { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + self.window + .update(&mut self.cx, |_, _, cx| cx.new(build_entity)) + .expect("window was unexpectedly closed") + } + + fn reserve_entity(&mut self) -> crate::Reservation { + self.cx.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: crate::Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + self.window + .update(&mut self.cx, |_, _, cx| { + cx.insert_entity(reservation, build_entity) + }) + .expect("window was unexpectedly closed") + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R + where + T: 'static, + { + self.cx.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> super::GpuiBorrow<'a, T> + where + T: 'static, + { + self.cx.as_mut(handle) + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + self.cx.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + self.cx.update_window(window, f) + } + + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + self.cx.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + self.cx.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.cx.background_spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + self.cx.read_global(callback) + } +} + +impl VisualContext for VisualTestContext { + type Result = T; + + /// Get the underlying window handle underlying this context. + fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Entity { + self.window + .update(&mut self.cx, |_, window, cx| { + cx.new(|cx| build_entity(window, cx)) + }) + .expect("window was unexpectedly closed") + } + + fn update_window_entity( + &mut self, + view: &Entity, + update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, + ) -> R { + let view = view.clone(); + self.cx + .app + .borrow_mut() + .with_window(view.entity_id(), |window, app| { + view.update(app, |v, cx| update(v, window, cx)) + }) + .expect("entity has no current window; use `update` instead of `update_in`") + } + + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Entity + where + V: 'static + Render, + { + self.window + .update(&mut self.cx, |_, window, cx| { + window.replace_root(cx, build_view) + }) + .expect("window was unexpectedly closed") + } + + fn focus(&mut self, view: &Entity) { + self.window + .update(&mut self.cx, |_, window, cx| { + view.read(cx).focus_handle(cx).focus(window, cx) + }) + .expect("window was unexpectedly closed") + } +} + +impl AnyWindowHandle { + /// Creates the given view in this window. + pub fn build_entity( + &self, + cx: &mut TestAppContext, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Entity { + self.update(cx, |_, window, cx| cx.new(|cx| build_view(window, cx))) + .unwrap() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + PathPromptOptions, SystemNotification, SystemNotificationAction, + SystemNotificationResponse, TestAppContext, + }; + use std::cell::RefCell; + use std::path::PathBuf; + use std::rc::Rc; + + #[gpui::test] + async fn test_system_notifications_require_identity_and_replace_matching_tags( + cx: &mut TestAppContext, + ) { + cx.update(|cx| { + cx.show_system_notification(SystemNotification { + tag: "thread-1".into(), + title: "Task started".into(), + body: "Running tests".into(), + actions: Vec::new(), + }); + }); + assert!(cx.shown_system_notifications().is_empty()); + assert!(cx.delivered_system_notifications().is_empty()); + + cx.update(|cx| { + cx.set_app_identity("com.example.tasks", "Tasks"); + cx.show_system_notification(SystemNotification { + tag: "thread-1".into(), + title: "Task started".into(), + body: "Running tests".into(), + actions: Vec::new(), + }); + cx.show_system_notification(SystemNotification { + tag: "thread-1".into(), + title: "Task finished".into(), + body: "All tests passed".into(), + actions: vec![SystemNotificationAction { + id: "open".into(), + label: "Open".into(), + }], + }); + }); + + assert_eq!( + cx.app_identity(), + Some(("com.example.tasks".into(), "Tasks".into())) + ); + assert_eq!(cx.shown_system_notifications().len(), 2); + assert_eq!( + cx.delivered_system_notifications(), + [SystemNotification { + tag: "thread-1".into(), + title: "Task finished".into(), + body: "All tests passed".into(), + actions: vec![SystemNotificationAction { + id: "open".into(), + label: "Open".into(), + }], + }] + ); + + cx.update(|cx| cx.dismiss_system_notification("thread-1")); + assert!(cx.delivered_system_notifications().is_empty()); + assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]); + } + + #[gpui::test] + async fn test_system_notification_body_and_action_responses(cx: &mut TestAppContext) { + let responses = Rc::new(RefCell::new(Vec::new())); + cx.update(|cx| { + cx.on_system_notification_response({ + let responses = responses.clone(); + move |response, _cx| responses.borrow_mut().push(response) + }); + }); + + cx.simulate_system_notification_response(SystemNotificationResponse { + tag: "thread-1".into(), + action_id: None, + }); + cx.simulate_system_notification_response(SystemNotificationResponse { + tag: "thread-1".into(), + action_id: Some("default".into()), + }); + + assert_eq!( + responses.borrow().as_slice(), + &[ + SystemNotificationResponse { + tag: "thread-1".into(), + action_id: None, + }, + SystemNotificationResponse { + tag: "thread-1".into(), + action_id: Some("default".into()), + }, + ] + ); + } + + #[gpui::test] + async fn test_system_notification_response_handler_can_be_replaced(cx: &mut TestAppContext) { + let first_responses = Rc::new(RefCell::new(Vec::new())); + let second_responses = Rc::new(RefCell::new(Vec::new())); + cx.update(|cx| { + cx.on_system_notification_response({ + let first_responses = first_responses.clone(); + move |response, _cx| first_responses.borrow_mut().push(response) + }); + cx.on_system_notification_response({ + let second_responses = second_responses.clone(); + move |response, _cx| second_responses.borrow_mut().push(response) + }); + }); + + let response = SystemNotificationResponse { + tag: "thread-1".into(), + action_id: None, + }; + cx.simulate_system_notification_response(response.clone()); + + assert!(first_responses.borrow().is_empty()); + assert_eq!(second_responses.borrow().as_slice(), &[response]); + } + + #[gpui::test] + async fn test_system_notification_response_handler_can_reenter_app(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.set_app_identity("com.example.tasks", "Tasks"); + cx.show_system_notification(SystemNotification { + tag: "thread-1".into(), + title: "Task finished".into(), + body: "All tests passed".into(), + actions: Vec::new(), + }); + cx.on_system_notification_response(|response, cx| { + cx.dismiss_system_notification(&response.tag); + }); + }); + + cx.simulate_system_notification_response(SystemNotificationResponse { + tag: "thread-1".into(), + action_id: None, + }); + + assert!(cx.delivered_system_notifications().is_empty()); + assert_eq!(cx.dismissed_system_notifications(), ["thread-1"]); + } + + #[gpui::test] + async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) { + assert!(!cx.did_prompt_for_paths()); + + let receiver = cx.update(|cx| { + cx.prompt_for_paths(PathPromptOptions { + files: false, + directories: true, + multiple: true, + prompt: None, + }) + }); + assert!(cx.did_prompt_for_paths()); + + let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")]; + cx.simulate_path_prompt_response({ + let selected = selected.clone(); + move |options| { + assert!(options.multiple); + Some(selected) + } + }); + assert!(!cx.did_prompt_for_paths()); + + let response = receiver.await.unwrap().unwrap(); + assert_eq!(response, Some(selected)); + } + + #[gpui::test] + async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) { + let receiver = cx.update(|cx| { + cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: false, + multiple: false, + prompt: None, + }) + }); + + cx.simulate_path_prompt_response(|_options| None); + + let response = receiver.await.unwrap().unwrap(); + assert_eq!(response, None); + } +} diff --git a/vendor/gpui-pre/src/app/visual_test_context.rs b/vendor/gpui-pre/src/app/visual_test_context.rs new file mode 100644 index 0000000..b54802a --- /dev/null +++ b/vendor/gpui-pre/src/app/visual_test_context.rs @@ -0,0 +1,484 @@ +use crate::{ + Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AssetSource, BackgroundExecutor, + Bounds, ClipboardItem, Context, Entity, EntityId, ForegroundExecutor, Global, InputEvent, + Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + Platform, Point, Render, Result, Size, Task, TestDispatcher, TextSystem, VisualTestPlatform, + Window, WindowBounds, WindowHandle, WindowOptions, app::GpuiMode, +}; +use anyhow::anyhow; +use image::RgbaImage; +use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; + +/// A test context that uses real macOS rendering instead of mocked rendering. +/// This is used for visual tests that need to capture actual screenshots. +/// +/// Unlike `TestAppContext` which uses `TestPlatform` with mocked rendering, +/// `VisualTestAppContext` uses the real `MacPlatform` to produce actual rendered output. +/// +/// Windows created through this context are positioned off-screen (at coordinates like -10000, -10000) +/// so they are invisible to the user but still fully rendered by the compositor. +#[derive(Clone)] +pub struct VisualTestAppContext { + /// The underlying app cell + pub app: Rc, + /// The background executor for running async tasks + pub background_executor: BackgroundExecutor, + /// The foreground executor for running tasks on the main thread + pub foreground_executor: ForegroundExecutor, + /// The test dispatcher for deterministic task scheduling + dispatcher: TestDispatcher, + platform: Rc, + text_system: Arc, +} + +impl VisualTestAppContext { + /// Creates a new `VisualTestAppContext` with real macOS platform rendering + /// but deterministic task scheduling via TestDispatcher. + /// + /// This provides: + /// - Real Metal/compositor rendering for accurate screenshots + /// - Deterministic task scheduling via TestDispatcher + /// - Controllable time via `advance_clock` + /// + /// Note: This uses a no-op asset source, so SVG icons won't render. + /// Use `with_asset_source` to provide real assets for icon rendering. + pub fn new(platform: Rc) -> Self { + Self::with_asset_source(platform, Arc::new(())) + } + + /// Creates a new `VisualTestAppContext` with a custom asset source. + /// + /// Use this when you need SVG icons to render properly in visual tests. + /// Pass the real `Assets` struct to enable icon rendering. + pub fn with_asset_source( + platform: Rc, + asset_source: Arc, + ) -> Self { + // Use a seeded RNG for deterministic behavior + let seed = std::env::var("SEED") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + // Create a visual test platform that combines real Mac rendering + // with controllable TestDispatcher for deterministic task scheduling + let platform = Rc::new(VisualTestPlatform::new(platform, seed)); + + // Get the dispatcher and executors from the platform + let dispatcher = platform.dispatcher().clone(); + let background_executor = platform.background_executor(); + let foreground_executor = platform.foreground_executor(); + + let text_system = Arc::new(TextSystem::new(platform.text_system())); + + let http_client = http_client::FakeHttpClient::with_404_response(); + + let mut app = App::new_app(platform.clone(), asset_source, http_client); + app.borrow_mut().mode = GpuiMode::test(); + + Self { + app, + background_executor, + foreground_executor, + dispatcher, + platform, + text_system, + } + } + + /// Opens a window positioned off-screen for invisible rendering. + /// + /// The window is positioned at (-10000, -10000) so it's not visible on any display, + /// but it's still fully rendered by the compositor and can be captured via ScreenCaptureKit. + /// + /// # Arguments + /// * `size` - The size of the window to create + /// * `build_root` - A closure that builds the root view for the window + pub fn open_offscreen_window( + &mut self, + size: Size, + build_root: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> Result> { + use crate::{point, px}; + + let bounds = Bounds { + origin: point(px(-10000.0), px(-10000.0)), + size, + }; + + let mut cx = self.app.borrow_mut(); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + focus: false, + show: true, + ..Default::default() + }, + build_root, + ) + } + + /// Opens an off-screen window with default size (1280x800). + pub fn open_offscreen_window_default( + &mut self, + build_root: impl FnOnce(&mut Window, &mut App) -> Entity, + ) -> Result> { + use crate::{px, size}; + self.open_offscreen_window(size(px(1280.0), px(800.0)), build_root) + } + + /// Returns whether screen capture is supported on this platform. + pub fn is_screen_capture_supported(&self) -> bool { + self.platform.is_screen_capture_supported() + } + + /// Returns the text system used by this context. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// Returns the background executor. + pub fn executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + /// Returns the foreground executor. + pub fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + /// Runs all pending foreground and background tasks until there's nothing left to do. + /// This is essential for processing async operations like tooltip timers. + pub fn run_until_parked(&self) { + self.dispatcher.run_until_parked(); + } + + /// Advances the simulated clock by the given duration and processes any tasks + /// that become ready. This is essential for testing time-based behaviors like + /// tooltip delays. + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.advance_clock(duration); + } + + /// Updates the app state. + pub fn update(&mut self, f: impl FnOnce(&mut App) -> R) -> R { + let mut app = self.app.borrow_mut(); + f(&mut app) + } + + /// Reads from the app state. + pub fn read(&self, f: impl FnOnce(&App) -> R) -> R { + let app = self.app.borrow(); + f(&app) + } + + /// Updates a window. + pub fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + /// Spawns a task on the foreground executor. + pub fn spawn(&self, f: F) -> Task + where + F: Future + 'static, + R: 'static, + { + self.foreground_executor.spawn(f) + } + + /// Checks if a global of type G exists. + pub fn has_global(&self) -> bool { + let app = self.app.borrow(); + app.has_global::() + } + + /// Reads a global value. + pub fn read_global(&self, f: impl FnOnce(&G, &App) -> R) -> R { + let app = self.app.borrow(); + f(app.global::(), &app) + } + + /// Sets a global value. + pub fn set_global(&mut self, global: G) { + let mut app = self.app.borrow_mut(); + app.set_global(global); + } + + /// Updates a global value. + pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R { + let mut lock = self.app.borrow_mut(); + lock.update(|cx| { + let mut global = cx.lease_global::(); + let result = f(&mut global, cx); + cx.end_global_lease(global); + result + }) + } + + /// Simulates a sequence of keystrokes on the given window. + /// + /// Keystrokes are specified as a space-separated string, e.g., "cmd-p escape". + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke_text in keystrokes.split_whitespace() { + let keystroke = Keystroke::parse(keystroke_text) + .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_text)); + self.dispatch_keystroke(window, keystroke); + } + self.run_until_parked(); + } + + /// Dispatches a single keystroke to a window. + pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) { + self.update_window(window, |_, window, cx| { + window.dispatch_keystroke(keystroke, cx); + }) + .ok(); + } + + /// Simulates typing text input on the given window. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for char in input.chars() { + let key = char.to_string(); + let keystroke = Keystroke { + modifiers: Modifiers::default(), + key: key.clone(), + key_char: Some(key), + }; + self.dispatch_keystroke(window, keystroke); + } + self.run_until_parked(); + } + + /// Simulates a mouse move event. + pub fn simulate_mouse_move( + &mut self, + window: AnyWindowHandle, + position: Point, + button: impl Into>, + modifiers: Modifiers, + ) { + self.simulate_event( + window, + MouseMoveEvent { + position, + modifiers, + pressed_button: button.into(), + }, + ); + } + + /// Simulates a mouse down event. + pub fn simulate_mouse_down( + &mut self, + window: AnyWindowHandle, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event( + window, + MouseDownEvent { + position, + modifiers, + button, + click_count: 1, + first_mouse: false, + }, + ); + } + + /// Simulates a mouse up event. + pub fn simulate_mouse_up( + &mut self, + window: AnyWindowHandle, + position: Point, + button: MouseButton, + modifiers: Modifiers, + ) { + self.simulate_event( + window, + MouseUpEvent { + position, + modifiers, + button, + click_count: 1, + }, + ); + } + + /// Simulates a click (mouse down followed by mouse up). + pub fn simulate_click( + &mut self, + window: AnyWindowHandle, + position: Point, + modifiers: Modifiers, + ) { + self.simulate_mouse_down(window, position, MouseButton::Left, modifiers); + self.simulate_mouse_up(window, position, MouseButton::Left, modifiers); + } + + /// Simulates an input event on the given window. + pub fn simulate_event(&mut self, window: AnyWindowHandle, event: E) { + self.update_window(window, |_, window, cx| { + window.dispatch_event(event.to_platform_input(), cx); + }) + .ok(); + self.run_until_parked(); + } + + /// Dispatches an action to the given window. + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: impl Action) { + self.update_window(window, |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx); + }) + .ok(); + self.run_until_parked(); + } + + /// Writes to the clipboard. + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.platform.write_to_clipboard(item); + } + + /// Reads from the clipboard. + pub fn read_from_clipboard(&self) -> Option { + self.platform.read_from_clipboard() + } + + /// Waits for a condition to become true, with a timeout. + pub async fn wait_for( + &mut self, + entity: &Entity, + predicate: impl Fn(&T) -> bool, + timeout: Duration, + ) -> Result<()> { + let start = web_time::Instant::now(); + loop { + { + let app = self.app.borrow(); + if predicate(entity.read(&app)) { + return Ok(()); + } + } + + if start.elapsed() > timeout { + return Err(anyhow!("Timed out waiting for condition")); + } + + self.run_until_parked(); + self.background_executor + .timer(Duration::from_millis(10)) + .await; + } + } + + /// Captures a screenshot of the specified window using direct texture capture. + /// + /// This renders the scene to a Metal texture and reads the pixels directly, + /// which does not require the window to be visible on screen. + #[cfg(any(test, feature = "test-support"))] + pub fn capture_screenshot(&mut self, window: AnyWindowHandle) -> Result { + self.update_window(window, |_, window, _cx| window.render_to_image())? + } + + /// Waits for animations to complete by waiting a couple of frames. + pub async fn wait_for_animations(&self) { + self.background_executor + .timer(Duration::from_millis(32)) + .await; + self.run_until_parked(); + } +} + +impl AppContext for VisualTestAppContext { + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { + let mut app = self.app.borrow_mut(); + app.new(build_entity) + } + + fn reserve_entity(&mut self) -> crate::Reservation { + let mut app = self.app.borrow_mut(); + app.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: crate::Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity { + let mut app = self.app.borrow_mut(); + app.insert_entity(reservation, build_entity) + } + + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R { + let mut app = self.app.borrow_mut(); + app.update_entity(handle, update) + } + + fn as_mut<'a, T>(&'a mut self, _: &Entity) -> crate::GpuiBorrow<'a, T> + where + T: 'static, + { + panic!("Cannot use as_mut with a visual test app context. Try calling update() first") + } + + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static, + { + let app = self.app.borrow(); + app.read_entity(handle, read) + } + + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T, + { + let mut lock = self.app.borrow_mut(); + lock.update_window(window, f) + } + + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option { + let mut lock = self.app.borrow_mut(); + lock.with_window(entity_id, f) + } + + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static, + { + let app = self.app.borrow(); + app.read_window(window, read) + } + + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.background_executor.spawn(future) + } + + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global, + { + let app = self.app.borrow(); + callback(app.global::(), &app) + } +} diff --git a/vendor/gpui-pre/src/arena.rs b/vendor/gpui-pre/src/arena.rs new file mode 100644 index 0000000..ea5968f --- /dev/null +++ b/vendor/gpui-pre/src/arena.rs @@ -0,0 +1,401 @@ +use std::{ + alloc::{self, handle_alloc_error}, + cell::Cell, + num::NonZeroUsize, + ops::{Deref, DerefMut}, + ptr::{self, NonNull}, + rc::Rc, +}; + +struct ArenaElement { + value: *mut u8, + drop: unsafe fn(*mut u8), +} + +impl Drop for ArenaElement { + #[inline(always)] + fn drop(&mut self) { + unsafe { (self.drop)(self.value) }; + } +} + +struct Chunk { + start: *mut u8, + end: *mut u8, + offset: *mut u8, +} + +impl Drop for Chunk { + fn drop(&mut self) { + unsafe { + let chunk_size = self.end.offset_from_unsigned(self.start); + // SAFETY: This succeeded during allocation. + let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1); + alloc::dealloc(self.start, layout); + } + } +} + +impl Chunk { + fn new(chunk_size: NonZeroUsize) -> Self { + // this only fails if chunk_size is unreasonably huge + let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap(); + let start = unsafe { alloc::alloc(layout) }; + if start.is_null() { + handle_alloc_error(layout); + } + let end = unsafe { start.add(chunk_size.get()) }; + Self { + start, + end, + offset: start, + } + } + + fn allocate(&mut self, layout: alloc::Layout) -> Option> { + // Compute the allocation bounds in integer address space so that the + // bounds check happens before any pointer offsetting. Offsetting a + // pointer past the end of its allocation is undefined behavior even if + // the result is never dereferenced (as happens on the chunk-spill + // path), so `ptr::add` cannot be used until we know the result stays + // in bounds. `checked_add` also handles the documented case where + // `align_offset` returns `usize::MAX`. + let base = self.offset.addr(); + let aligned_addr = base.checked_add(self.offset.align_offset(layout.align()))?; + let next_addr = aligned_addr.checked_add(layout.size())?; + + if next_addr <= self.end.addr() { + let aligned = self.offset.with_addr(aligned_addr); + self.offset = self.offset.with_addr(next_addr); + NonNull::new(aligned) + } else { + None + } + } + + fn reset(&mut self) { + self.offset = self.start; + } +} + +pub struct Arena { + chunks: Vec, + elements: Vec, + valid: Rc>, + current_chunk_index: usize, + chunk_size: NonZeroUsize, + scope_depth: usize, +} + +impl Drop for Arena { + fn drop(&mut self) { + self.force_clear(); + } +} + +impl Arena { + pub fn new(chunk_size: usize) -> Self { + let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap(); + Self { + chunks: vec![Chunk::new(chunk_size)], + elements: Vec::new(), + valid: Rc::new(Cell::new(true)), + current_chunk_index: 0, + chunk_size, + scope_depth: 0, + } + } + + pub fn capacity(&self) -> usize { + self.chunks.len() * self.chunk_size.get() + } + + /// Marks the start of a scope (e.g. a window draw) whose allocations must stay + /// live until the scope ends, even if `clear` is called by a nested scope in + /// the meantime. + pub fn begin_scope(&mut self) { + self.scope_depth += 1; + } + + /// Ends the innermost scope started with `begin_scope`. + /// + /// Panics if no scope is active: an unbalanced `end_scope` would let `clear` + /// run while an enclosing scope still references arena memory, which is + /// exactly the use-after-free this bookkeeping exists to prevent, so failing + /// loudly here is preferable. + pub fn end_scope(&mut self) { + self.scope_depth = self + .scope_depth + .checked_sub(1) + .expect("Arena::end_scope called without a matching begin_scope"); + } + + /// Drops all allocations and resets the arena, unless a scope is still active. + /// + /// When a draw triggers a nested draw (e.g. re-entrant window procedure + /// invocations on Windows, or opening a window from within a draw), the nested + /// draw's clear must not free memory the outer draw still references, so it is + /// deferred: the outer draw's own clear will drop both draws' allocations. + pub fn clear(&mut self) { + if self.scope_depth == 0 { + self.force_clear(); + } else { + log::debug!( + "deferring arena clear; {} enclosing scope(s) still active", + self.scope_depth + ); + } + } + + fn force_clear(&mut self) { + self.valid.set(false); + self.valid = Rc::new(Cell::new(true)); + self.elements.clear(); + for chunk_index in 0..=self.current_chunk_index { + self.chunks[chunk_index].reset(); + } + self.current_chunk_index = 0; + } + + #[inline(always)] + pub fn alloc(&mut self, f: impl FnOnce() -> T) -> ArenaBox { + #[inline(always)] + unsafe fn inner_writer(ptr: *mut T, f: F) + where + F: FnOnce() -> T, + { + unsafe { ptr::write(ptr, f()) }; + } + + unsafe fn drop(ptr: *mut u8) { + unsafe { std::ptr::drop_in_place(ptr.cast::()) }; + } + + let layout = alloc::Layout::new::(); + let current_chunk = &mut self.chunks[self.current_chunk_index]; + let ptr = if let Some(ptr) = current_chunk.allocate(layout) { + ptr.as_ptr() + } else { + self.allocate_next_chunk(layout) + }; + + unsafe { inner_writer(ptr.cast(), f) }; + self.elements.push(ArenaElement { + value: ptr, + drop: drop::, + }); + + ArenaBox { + ptr: ptr.cast(), + valid: self.valid.clone(), + } + } + + #[inline(never)] + fn allocate_next_chunk(&mut self, layout: alloc::Layout) -> *mut u8 { + self.current_chunk_index += 1; + if self.current_chunk_index >= self.chunks.len() { + self.chunks.push(Chunk::new(self.chunk_size)); + assert_eq!(self.current_chunk_index, self.chunks.len() - 1); + log::trace!( + "increased element arena capacity to {}kb", + self.capacity() / 1024, + ); + } + let current_chunk = &mut self.chunks[self.current_chunk_index]; + if let Some(pointer) = current_chunk.allocate(layout) { + pointer.as_ptr() + } else { + panic!( + "Arena chunk_size of {} is too small to allocate {} bytes", + self.chunk_size, + layout.size() + ); + } + } +} + +pub struct ArenaBox { + ptr: *mut T, + valid: Rc>, +} + +impl ArenaBox { + #[inline(always)] + pub fn map(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaBox { + ArenaBox { + ptr: f(&mut self), + valid: self.valid, + } + } + + #[track_caller] + fn validate(&self) { + assert!( + self.valid.get(), + "attempted to dereference an ArenaRef after its Arena was cleared" + ); + } +} + +impl Deref for ArenaBox { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.validate(); + unsafe { &*self.ptr } + } +} + +impl DerefMut for ArenaBox { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + self.validate(); + unsafe { &mut *self.ptr } + } +} + +#[cfg(test)] +mod tests { + use std::{cell::Cell, rc::Rc}; + + use super::*; + + #[test] + fn test_arena() { + let mut arena = Arena::new(1024); + let a = arena.alloc(|| 1u64); + let b = arena.alloc(|| 2u32); + let c = arena.alloc(|| 3u16); + let d = arena.alloc(|| 4u8); + assert_eq!(*a, 1); + assert_eq!(*b, 2); + assert_eq!(*c, 3); + assert_eq!(*d, 4); + + arena.clear(); + let a = arena.alloc(|| 5u64); + let b = arena.alloc(|| 6u32); + let c = arena.alloc(|| 7u16); + let d = arena.alloc(|| 8u8); + assert_eq!(*a, 5); + assert_eq!(*b, 6); + assert_eq!(*c, 7); + assert_eq!(*d, 8); + + // Ensure drop gets called. + let dropped = Rc::new(Cell::new(false)); + struct DropGuard(Rc>); + impl Drop for DropGuard { + fn drop(&mut self) { + self.0.set(true); + } + } + arena.alloc(|| DropGuard(dropped.clone())); + arena.clear(); + assert!(dropped.get()); + } + + #[test] + fn test_arena_grow() { + let mut arena = Arena::new(8); + arena.alloc(|| 1u64); + arena.alloc(|| 2u64); + + assert_eq!(arena.capacity(), 16); + + arena.alloc(|| 3u32); + arena.alloc(|| 4u32); + + assert_eq!(arena.capacity(), 24); + } + + #[test] + fn test_arena_alignment() { + let mut arena = Arena::new(256); + let x1 = arena.alloc(|| 1u8); + let x2 = arena.alloc(|| 2u16); + let x3 = arena.alloc(|| 3u32); + let x4 = arena.alloc(|| 4u64); + let x5 = arena.alloc(|| 5u64); + + assert_eq!(*x1, 1); + assert_eq!(*x2, 2); + assert_eq!(*x3, 3); + assert_eq!(*x4, 4); + assert_eq!(*x5, 5); + + assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0); + assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0); + } + + #[test] + #[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")] + fn test_arena_use_after_clear() { + let mut arena = Arena::new(16); + let value = arena.alloc(|| 1u64); + + arena.clear(); + let _read_value = *value; + } + + #[test] + fn test_clear_deferred_while_scope_active() { + struct DropCounter(Rc>); + impl Drop for DropCounter { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + let drops = Rc::new(Cell::new(0)); + let mut arena = Arena::new(1024); + + // Outer draw starts and allocates. + arena.begin_scope(); + let outer = arena.alloc(|| 42u64); + arena.alloc({ + let drops = drops.clone(); + || DropCounter(drops) + }); + + // Nested draw runs to completion and requests a clear. + arena.begin_scope(); + let inner = arena.alloc(|| 7u64); + arena.alloc({ + let drops = drops.clone(); + || DropCounter(drops) + }); + arena.end_scope(); + arena.clear(); + + // The clear must be deferred: the outer draw's allocations are still live. + assert_eq!(*outer, 42); + assert_eq!(*inner, 7); + assert_eq!(drops.get(), 0); + + // Once the outer draw finishes, its clear drops both draws' allocations. + arena.end_scope(); + arena.clear(); + assert_eq!(drops.get(), 2); + } + + #[test] + fn test_clear_without_scope_is_immediate() { + let mut arena = Arena::new(1024); + let value = arena.alloc(|| 1u64); + assert_eq!(*value, 1); + arena.clear(); + assert!(!value.valid.get()); + } + + #[test] + #[should_panic(expected = "Arena::end_scope called without a matching begin_scope")] + fn test_unbalanced_end_scope_panics() { + let mut arena = Arena::new(1024); + arena.begin_scope(); + arena.end_scope(); + arena.end_scope(); + } +} diff --git a/vendor/gpui-pre/src/asset_cache.rs b/vendor/gpui-pre/src/asset_cache.rs new file mode 100644 index 0000000..4c7bee1 --- /dev/null +++ b/vendor/gpui-pre/src/asset_cache.rs @@ -0,0 +1,358 @@ +use crate::{App, EntityId, SharedString, SharedUri, Task}; +use collections::FxHashSet; +use futures::{Future, TryFutureExt}; + +use std::cell::RefCell; +use std::fmt::Debug; +use std::hash::{BuildHasher, Hash}; +use std::marker::PhantomData; +use std::mem; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::Arc; + +pub(crate) struct CachedLoad { + state: Rc>>, + // Keep the owner outside the state so publishing a result cannot cancel its own task. + _task: Task<()>, +} + +enum CachedLoadState { + Loading(FxHashSet), + Loaded(T), +} + +impl CachedLoad { + pub(crate) fn new(future: impl Future + Send + 'static, cx: &App) -> Self { + let state = Rc::new(RefCell::new(CachedLoadState::Loading(FxHashSet::default()))); + let task = cx.background_executor().spawn(future); + let task = cx.spawn({ + let state = Rc::downgrade(&state); + async move |cx| { + let result = task.await; + let Some(state) = state.upgrade() else { + return; + }; + let previous = + mem::replace(&mut *state.borrow_mut(), CachedLoadState::Loaded(result)); + let CachedLoadState::Loading(views) = previous else { + unreachable!("a cached load completes only once"); + }; + cx.update(|cx| { + for view in views { + cx.notify(view); + } + }); + } + }); + Self { state, _task: task } + } + + pub(crate) fn get(&self) -> Option { + match &*self.state.borrow() { + CachedLoadState::Loading(_) => None, + CachedLoadState::Loaded(result) => Some(result.clone()), + } + } + + pub(crate) fn use_by(&self, view: EntityId) -> Option { + match &mut *self.state.borrow_mut() { + CachedLoadState::Loading(views) => { + views.insert(view); + None + } + CachedLoadState::Loaded(result) => Some(result.clone()), + } + } +} + +/// An enum representing +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub enum Resource { + /// This resource is at a given URI + Uri(SharedUri), + /// This resource is at a given path in the file system + Path(Arc), + /// This resource is embedded in the application binary + Embedded(SharedString), +} + +impl From for Resource { + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } +} + +impl From for Resource { + fn from(value: PathBuf) -> Self { + Self::Path(value.into()) + } +} + +impl From> for Resource { + fn from(value: Arc) -> Self { + Self::Path(value) + } +} + +/// A trait for asynchronous asset loading. +pub trait Asset: 'static { + /// The source of the asset. + type Source: Clone + Hash + Send; + + /// The loaded asset + type Output: Clone + Send; + + /// Load the asset asynchronously + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static; +} + +/// An asset Loader which logs the [`Err`] variant of a [`Result`] during loading +pub enum AssetLogger { + #[doc(hidden)] + _Phantom(PhantomData, &'static dyn crate::seal::Sealed), +} + +impl Asset for AssetLogger +where + T: Asset>, + R: Clone + Send, + E: Clone + Send + Debug, +{ + type Source = T::Source; + + type Output = T::Output; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let load = T::load(source, cx); + load.inspect_err(|e| log::error!("Failed to load asset: {:?}", e)) + } +} + +/// Use a quick, non-cryptographically secure hash function to get an identifier from data +pub fn hash(data: &T) -> u64 { + collections::FxBuildHasher.hash_one(data) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AppContext, TestAppContext}; + use futures::channel::oneshot; + use std::{ + cell::Cell, + collections::VecDeque, + hash::{Hash, Hasher}, + rc::Rc, + sync::{ + Arc, Mutex, + atomic::{AtomicUsize, Ordering}, + }, + }; + + #[gpui::test] + fn cached_load_caches_success_and_error(cx: &mut TestAppContext) { + let successful_load = + cx.update(|cx| CachedLoad::new(async { Ok::<_, &'static str>(42) }, cx)); + let failed_load = + cx.update(|cx| CachedLoad::new(async { Err::("load failed") }, cx)); + + assert_eq!(successful_load.get(), None); + assert_eq!(failed_load.get(), None); + + cx.run_until_parked(); + + assert_eq!(successful_load.get(), Some(Ok(42))); + assert_eq!(successful_load.get(), Some(Ok(42))); + assert_eq!(failed_load.get(), Some(Err("load failed"))); + assert_eq!(failed_load.get(), Some(Err("load failed"))); + } + + #[gpui::test] + fn completed_load_without_subscribers_is_cached_and_deduplicated(cx: &mut TestAppContext) { + let (sender, receiver) = oneshot::channel(); + let source = TestAssetSource::new(1, [receiver]); + + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + assert_eq!(source.load_count(), 1); + + assert!(sender.send(Ok(42)).is_ok()); + cx.run_until_parked(); + + assert_eq!( + cx.update(|cx| cx.fetch_asset::(&source)), + Some(Ok(42)) + ); + assert_eq!( + cx.update(|cx| cx.fetch_asset::(&source)), + Some(Ok(42)) + ); + assert_eq!(source.load_count(), 1); + } + + #[gpui::test] + fn load_completion_notifies_each_observing_entity_once(cx: &mut TestAppContext) { + let (sender, receiver) = oneshot::channel::(); + let load = cx.update(|cx| { + CachedLoad::new( + async move { receiver.await.expect("test sends a result") }, + cx, + ) + }); + let first_notification_count = Rc::new(Cell::new(0)); + let second_notification_count = Rc::new(Cell::new(0)); + let (first_entity, second_entity) = cx.update(|cx| { + let first_entity = cx.new(|_| ()); + let second_entity = cx.new(|_| ()); + cx.observe(&first_entity, { + let first_notification_count = first_notification_count.clone(); + move |_, _| first_notification_count.set(first_notification_count.get() + 1) + }) + .detach(); + cx.observe(&second_entity, { + let second_notification_count = second_notification_count.clone(); + move |_, _| second_notification_count.set(second_notification_count.get() + 1) + }) + .detach(); + (first_entity, second_entity) + }); + + assert_eq!(load.use_by(first_entity.entity_id()), None); + assert_eq!(load.use_by(first_entity.entity_id()), None); + assert_eq!(load.use_by(second_entity.entity_id()), None); + + assert!(sender.send(42).is_ok()); + cx.run_until_parked(); + + assert_eq!(first_notification_count.get(), 1); + assert_eq!(second_notification_count.get(), 1); + assert_eq!(load.use_by(first_entity.entity_id()), Some(42)); + assert_eq!(load.use_by(second_entity.entity_id()), Some(42)); + } + + #[gpui::test] + fn removing_pending_asset_cancels_it_and_replacement_ignores_stale_completion( + cx: &mut TestAppContext, + ) { + let (stale_sender, stale_receiver) = oneshot::channel(); + let (replacement_sender, replacement_receiver) = oneshot::channel(); + let source = TestAssetSource::new(2, [stale_receiver, replacement_receiver]); + + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + assert_eq!(source.load_count(), 1); + cx.run_until_parked(); + + cx.update(|cx| cx.remove_asset::(&source)); + assert!(!cx.update(|cx| cx.has_asset::(&source))); + cx.run_until_parked(); + assert!(stale_sender.send(Ok(1)).is_err()); + + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + assert_eq!(source.load_count(), 2); + + assert!(replacement_sender.send(Err("replacement failed")).is_ok()); + cx.run_until_parked(); + + assert_eq!( + cx.update(|cx| cx.fetch_asset::(&source)), + Some(Err("replacement failed")) + ); + assert_eq!(source.load_count(), 2); + } + + #[gpui::test] + fn eviction_after_worker_completion_does_not_publish_into_replacement(cx: &mut TestAppContext) { + let (sender, receiver) = oneshot::channel(); + let (replacement_sender, replacement_receiver) = oneshot::channel(); + let source = TestAssetSource::new(3, [receiver, replacement_receiver]); + + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + assert!(sender.send(Ok(1)).is_ok()); + while source.completion_count.load(Ordering::SeqCst) == 0 { + assert!(cx.background_executor.tick()); + } + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + + cx.update(|cx| { + cx.remove_asset::(&source); + assert_eq!(cx.fetch_asset::(&source), None); + }); + cx.run_until_parked(); + assert_eq!(cx.update(|cx| cx.fetch_asset::(&source)), None); + + assert!(replacement_sender.send(Ok(2)).is_ok()); + cx.run_until_parked(); + assert_eq!( + cx.update(|cx| cx.fetch_asset::(&source)), + Some(Ok(2)) + ); + assert_eq!(source.load_count(), 2); + } + + struct TestAsset; + + #[derive(Clone)] + struct TestAssetSource { + id: usize, + load_count: Arc, + completion_count: Arc, + receivers: Arc>>>>, + } + + impl TestAssetSource { + fn new( + id: usize, + receivers: impl IntoIterator>>, + ) -> Self { + Self { + id, + load_count: Arc::new(AtomicUsize::new(0)), + completion_count: Arc::new(AtomicUsize::new(0)), + receivers: Arc::new(Mutex::new(receivers.into_iter().collect())), + } + } + + fn load_count(&self) -> usize { + self.load_count.load(Ordering::SeqCst) + } + } + + impl Hash for TestAssetSource { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } + } + + impl Asset for TestAsset { + type Source = TestAssetSource; + type Output = Result; + + fn load( + source: Self::Source, + _cx: &mut App, + ) -> impl Future + Send + 'static { + source.load_count.fetch_add(1, Ordering::SeqCst); + let receiver = source + .receivers + .lock() + .expect("test receiver mutex should not be poisoned") + .pop_front() + .expect("each test load should have a receiver"); + async move { + let result = match receiver.await { + Ok(result) => result, + Err(_) => Err("cancelled"), + }; + source.completion_count.fetch_add(1, Ordering::SeqCst); + result + } + } + } +} diff --git a/vendor/gpui-pre/src/assets.rs b/vendor/gpui-pre/src/assets.rs new file mode 100644 index 0000000..cb8b47e --- /dev/null +++ b/vendor/gpui-pre/src/assets.rs @@ -0,0 +1,133 @@ +use crate::{DevicePixels, Pixels, Result, SharedString, Size, size}; +use smallvec::SmallVec; + +use image::{Delay, Frame}; +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, +}; + +/// A source of assets for this app to use. +pub trait AssetSource: 'static + Send + Sync { + /// Load the given asset from the source path. + fn load(&self, path: &str) -> Result>>; + + /// List the assets at the given path. + fn list(&self, path: &str) -> Result>; +} + +impl AssetSource for () { + fn load(&self, _path: &str) -> Result>> { + Ok(None) + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +/// A unique identifier for the image cache +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ImageId(pub usize); + +#[derive(PartialEq, Eq, Hash, Clone)] +#[expect(missing_docs)] +pub struct RenderImageParams { + pub image_id: ImageId, + pub frame_index: usize, +} + +/// A cached and processed image, in BGRA format +pub struct RenderImage { + /// The ID associated with this image + pub id: ImageId, + /// The scale factor of this image on render. + pub(crate) scale_factor: f32, + data: SmallVec<[Frame; 1]>, +} + +impl PartialEq for RenderImage { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for RenderImage {} + +impl RenderImage { + /// Create a new image from the given data. + pub fn new(data: impl Into>) -> Self { + static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + + Self { + id: ImageId(NEXT_ID.fetch_add(1, SeqCst)), + scale_factor: 1.0, + data: data.into(), + } + } + + /// Convert this image into a byte slice. + pub fn as_bytes(&self, frame_index: usize) -> Option<&[u8]> { + self.data + .get(frame_index) + .map(|frame| frame.buffer().as_raw().as_slice()) + } + + /// Get the size of this image, in pixels. + pub fn size(&self, frame_index: usize) -> Size { + self.data + .get(frame_index) + .map(|frame| { + let (width, height) = frame.buffer().dimensions(); + size(width.into(), height.into()) + }) + .unwrap_or_default() + } + + /// Get the size of this image, in pixels for display, adjusted for the scale factor. + pub(crate) fn render_size(&self, frame_index: usize) -> Size { + self.size(frame_index) + .map(|v| (v.0 as f32 / self.scale_factor).into()) + } + + /// Get the delay of this frame from the previous + pub fn delay(&self, frame_index: usize) -> Delay { + self.data + .get(frame_index) + .map(|frame| frame.delay()) + .unwrap_or(Delay::from_numer_denom_ms(100, 1)) + } + + /// Get the number of frames for this image. + pub fn frame_count(&self) -> usize { + self.data.len() + } +} + +impl fmt::Debug for RenderImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageData") + .field("id", &self.id) + .field("size", &self.data.first().map(|f| f.buffer().dimensions())) + .finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use smallvec::SmallVec; + + #[test] + fn empty_render_image_does_not_panic() { + let image = RenderImage::new(SmallVec::new()); + assert_eq!(image.frame_count(), 0); + assert_eq!(image.size(0), Size::default()); + assert_eq!(image.as_bytes(0), None); + assert_eq!(image.render_size(0), Size::default()); + assert_eq!(image.delay(0), Delay::from_numer_denom_ms(100, 1)); + let _ = format!("{image:?}"); + } +} diff --git a/vendor/gpui-pre/src/bounds_tree.rs b/vendor/gpui-pre/src/bounds_tree.rs new file mode 100644 index 0000000..e95e324 --- /dev/null +++ b/vendor/gpui-pre/src/bounds_tree.rs @@ -0,0 +1,472 @@ +use crate::{Bounds, Half}; +use std::{ + cmp, + fmt::Debug, + ops::{Add, Sub}, + ptr::NonNull, +}; + +/// Maximum children per internal node (R-tree style branching factor). +/// Higher values = shorter tree = fewer cache misses, but more work per node. +const MAX_CHILDREN: usize = 12; + +/// A spatial tree optimized for finding maximum ordering among intersecting bounds. +/// +/// This is an R-tree variant specifically designed for the use case of assigning +/// z-order to overlapping UI elements. Key optimizations: +/// - Tracks the leaf with global max ordering for O(1) fast-path queries +/// - Uses higher branching factor (4) for lower tree height +/// - Aggressive pruning during search based on max_order metadata +#[derive(Debug)] +pub(crate) struct BoundsTree +where + U: Clone + Debug + Default + PartialEq, +{ + /// All nodes stored contiguously for cache efficiency. + nodes: Vec>, + /// Index of the root node, if any. + root: Option, + /// Index of the leaf with the highest ordering (for fast-path lookups). + max_leaf: Option, + /// Reusable stack for tree traversal during insertion. + insert_path: Vec, + /// Reusable stack for search operations. + search_stack: Vec>>, +} + +/// A node in the bounds tree. +#[derive(Debug, Clone)] +struct Node +where + U: Clone + Debug + Default + PartialEq, +{ + /// Bounding box containing this node and all descendants. + bounds: Bounds, + /// Maximum ordering value in this subtree. + max_order: u32, + /// Node-specific data. + kind: NodeKind, +} + +#[derive(Debug, Clone)] +enum NodeKind { + /// Leaf node containing actual bounds data. + Leaf { + /// The ordering assigned to this bounds. + order: u32, + }, + /// Internal node with children. + Internal { + /// Indices of child nodes (2 to MAX_CHILDREN). + children: NodeChildren, + }, +} + +/// Fixed-size array for child indices, avoiding heap allocation. +#[derive(Debug, Clone)] +struct NodeChildren { + // Keeps an invariant where the max order child is always at the end + indices: [usize; MAX_CHILDREN], + len: u8, +} + +impl NodeChildren { + fn new() -> Self { + Self { + indices: [0; MAX_CHILDREN], + len: 0, + } + } + + fn push(&mut self, index: usize) { + debug_assert!((self.len as usize) < MAX_CHILDREN); + self.indices[self.len as usize] = index; + self.len += 1; + } + + fn len(&self) -> usize { + self.len as usize + } + + fn as_slice(&self) -> &[usize] { + &self.indices[..self.len as usize] + } +} + +impl BoundsTree +where + U: Clone + + Debug + + PartialEq + + PartialOrd + + Add + + Sub + + Half + + Default, +{ + /// Clears all nodes from the tree. + pub fn clear(&mut self) { + self.nodes.clear(); + self.root = None; + self.max_leaf = None; + self.insert_path.clear(); + self.search_stack.clear(); + } + + /// Inserts bounds into the tree and returns its assigned ordering. + /// + /// The ordering is one greater than the maximum ordering of any + /// existing bounds that intersect with the new bounds. + pub fn insert(&mut self, new_bounds: Bounds) -> u32 { + // Find maximum ordering among intersecting bounds + let max_intersecting = self.find_max_ordering(&new_bounds); + let ordering = max_intersecting + 1; + + // Insert the new leaf + let new_leaf_idx = self.insert_leaf(new_bounds, ordering); + + // Update max_leaf tracking + self.max_leaf = match self.max_leaf { + None => Some(new_leaf_idx), + Some(old_idx) if self.nodes[old_idx].max_order < ordering => Some(new_leaf_idx), + some => some, + }; + + ordering + } + + /// Finds the maximum ordering among all bounds that intersect with the query. + fn find_max_ordering(&mut self, query: &Bounds) -> u32 { + let Some(root_idx) = self.root else { + return 0; + }; + + // Fast path: check if the max-ordering leaf intersects + if let Some(max_idx) = self.max_leaf { + let max_node = &self.nodes[max_idx]; + if query.intersects(&max_node.bounds) { + return max_node.max_order; + } + } + + // Slow path: search the tree + self.search_stack.clear(); + self.search_stack.push(NonNull::from(&self.nodes[root_idx])); + + let mut max_found = 0u32; + + while let Some(node) = self.search_stack.pop() { + // SAFETY: `node` is guaranteed to be valid as the `nodes` stack is unmodified in this function + // and the `search_stack` only contains pointers from this function call. + let node = unsafe { node.as_ref() }; + + // Pruning: skip if this subtree can't improve our result + if node.max_order <= max_found { + continue; + } + + // Spatial pruning: skip if bounds don't intersect + if !query.intersects(&node.bounds) { + continue; + } + + match &node.kind { + NodeKind::Leaf { order } => { + max_found = cmp::max(max_found, *order); + } + NodeKind::Internal { children } => { + // Children are maintained with highest max_order at the end. + // Push in forward order to highest (last) is popped first. + self.search_stack.extend( + children + .as_slice() + .iter() + .map(|&child_idx| &self.nodes[child_idx]) + .filter(|node| node.max_order > max_found) + .map(NonNull::from), + ); + } + } + } + + max_found + } + + /// Inserts a leaf node with the given bounds and ordering. + /// Returns the index of the new leaf. + fn insert_leaf(&mut self, bounds: Bounds, order: u32) -> usize { + let new_leaf_idx = self.nodes.len(); + self.nodes.push(Node { + bounds: bounds.clone(), + max_order: order, + kind: NodeKind::Leaf { order }, + }); + + let Some(root_idx) = self.root else { + // Tree is empty, new leaf becomes root + self.root = Some(new_leaf_idx); + return new_leaf_idx; + }; + + // If root is a leaf, create internal node with both + if matches!(self.nodes[root_idx].kind, NodeKind::Leaf { .. }) { + let root_bounds = self.nodes[root_idx].bounds.clone(); + let root_order = self.nodes[root_idx].max_order; + + let mut children = NodeChildren::new(); + // Max end invariant + if order > root_order { + children.push(root_idx); + children.push(new_leaf_idx); + } else { + children.push(new_leaf_idx); + children.push(root_idx); + } + + let new_root_idx = self.nodes.len(); + self.nodes.push(Node { + bounds: root_bounds.union(&bounds), + max_order: cmp::max(root_order, order), + kind: NodeKind::Internal { children }, + }); + self.root = Some(new_root_idx); + return new_leaf_idx; + } + + // Descend to find the best internal node to insert into + self.insert_path.clear(); + let mut current_idx = root_idx; + + loop { + let current = &self.nodes[current_idx]; + let NodeKind::Internal { children } = ¤t.kind else { + unreachable!("Should only traverse internal nodes"); + }; + + self.insert_path.push(current_idx); + + // Find the best child to descend into + let mut best_child_idx = children.as_slice()[0]; + let mut best_child_pos = 0; + let mut best_cost = bounds + .union(&self.nodes[best_child_idx].bounds) + .half_perimeter(); + + for (pos, &child_idx) in children.as_slice().iter().enumerate().skip(1) { + let cost = bounds.union(&self.nodes[child_idx].bounds).half_perimeter(); + if cost < best_cost { + best_cost = cost; + best_child_idx = child_idx; + best_child_pos = pos; + } + } + + // Check if best child is a leaf or internal + if matches!(self.nodes[best_child_idx].kind, NodeKind::Leaf { .. }) { + // Best child is a leaf. Check if current node has room for another child. + if children.len() < MAX_CHILDREN { + // Add new leaf directly to this node + let node = &mut self.nodes[current_idx]; + + if let NodeKind::Internal { children } = &mut node.kind { + children.push(new_leaf_idx); + // Swap new leaf only if it has the highest max_order + if order <= node.max_order { + let last = children.len() - 1; + children.indices.swap(last - 1, last); + } + } + + node.bounds = node.bounds.union(&bounds); + node.max_order = cmp::max(node.max_order, order); + break; + } else { + // Node is full, create new internal with [best_leaf, new_leaf] + let sibling_bounds = self.nodes[best_child_idx].bounds.clone(); + let sibling_order = self.nodes[best_child_idx].max_order; + + let mut new_children = NodeChildren::new(); + // Max end invariant + if order > sibling_order { + new_children.push(best_child_idx); + new_children.push(new_leaf_idx); + } else { + new_children.push(new_leaf_idx); + new_children.push(best_child_idx); + } + + let new_internal_idx = self.nodes.len(); + let new_internal_max = cmp::max(sibling_order, order); + self.nodes.push(Node { + bounds: sibling_bounds.union(&bounds), + max_order: new_internal_max, + kind: NodeKind::Internal { + children: new_children, + }, + }); + + // Replace the leaf with the new internal in parent + let parent = &mut self.nodes[current_idx]; + if let NodeKind::Internal { children } = &mut parent.kind { + let children_len = children.len(); + + children.indices[best_child_pos] = new_internal_idx; + + // If new internal has highest max_order, swap it to the end + // to maintain sorting invariant + if new_internal_max > parent.max_order { + children.indices.swap(best_child_pos, children_len - 1); + } + } + break; + } + } else { + // Best child is internal, continue descent + current_idx = best_child_idx; + } + } + + // Propagate bounds and max_order updates up the tree + let mut updated_child_idx = None; + for &node_idx in self.insert_path.iter().rev() { + let node = &mut self.nodes[node_idx]; + node.bounds = node.bounds.union(&bounds); + + if node.max_order < order { + node.max_order = order; + + // Swap updated child to end (skip first iteration since the invariant is already handled by previous cases) + if let Some(child_idx) = updated_child_idx { + if let NodeKind::Internal { children } = &mut node.kind { + if let Some(pos) = children.as_slice().iter().position(|&c| c == child_idx) + { + let last = children.len() - 1; + if pos != last { + children.indices.swap(pos, last); + } + } + } + } + } + + updated_child_idx = Some(node_idx); + } + + new_leaf_idx + } +} + +impl Default for BoundsTree +where + U: Clone + Debug + Default + PartialEq, +{ + fn default() -> Self { + BoundsTree { + nodes: Vec::new(), + root: None, + max_leaf: None, + insert_path: Vec::new(), + search_stack: Vec::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Bounds, Point, Size}; + use rand::{Rng, SeedableRng}; + + #[test] + fn test_insert() { + let mut tree = BoundsTree::::default(); + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 5.0, y: 5.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + + // Insert the bounds into the tree and verify the order is correct + assert_eq!(tree.insert(bounds1), 1); + assert_eq!(tree.insert(bounds2), 2); + assert_eq!(tree.insert(bounds3), 3); + + // Insert non-overlapping bounds and verify they can reuse orders + let bounds4 = Bounds { + origin: Point { x: 20.0, y: 20.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds5 = Bounds { + origin: Point { x: 40.0, y: 40.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + let bounds6 = Bounds { + origin: Point { x: 25.0, y: 25.0 }, + size: Size { + width: 10.0, + height: 10.0, + }, + }; + assert_eq!(tree.insert(bounds4), 1); // bounds4 does not overlap with bounds1, bounds2, or bounds3 + assert_eq!(tree.insert(bounds5), 1); // bounds5 does not overlap with any other bounds + assert_eq!(tree.insert(bounds6), 2); // bounds6 overlaps with bounds4, so it should have a different order + } + + #[test] + fn test_random_iterations() { + let max_bounds = 100; + for seed in 1..=1000 { + // let seed = 44; + let mut tree = BoundsTree::default(); + let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); + let mut expected_quads: Vec<(Bounds, u32)> = Vec::new(); + + // Insert a random number of random AABBs into the tree. + let num_bounds = rng.random_range(1..=max_bounds); + for _ in 0..num_bounds { + let min_x: f32 = rng.random_range(-100.0..100.0); + let min_y: f32 = rng.random_range(-100.0..100.0); + let width: f32 = rng.random_range(0.0..50.0); + let height: f32 = rng.random_range(0.0..50.0); + let bounds = Bounds { + origin: Point { x: min_x, y: min_y }, + size: Size { width, height }, + }; + + let expected_ordering = expected_quads + .iter() + .filter_map(|quad| quad.0.intersects(&bounds).then_some(quad.1)) + .max() + .unwrap_or(0) + + 1; + expected_quads.push((bounds, expected_ordering)); + + // Insert the AABB into the tree and collect intersections. + let actual_ordering = tree.insert(bounds); + assert_eq!(actual_ordering, expected_ordering); + } + } + } +} diff --git a/vendor/gpui-pre/src/color.rs b/vendor/gpui-pre/src/color.rs new file mode 100644 index 0000000..3bd893f --- /dev/null +++ b/vendor/gpui-pre/src/color.rs @@ -0,0 +1,1070 @@ +use anyhow::{Context as _, bail}; +use schemars::{JsonSchema, json_schema}; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{self, Visitor}, +}; +use std::borrow::Cow; +use std::{ + fmt::{self, Display, Formatter}, + hash::{Hash, Hasher}, +}; + +/// Convert an RGB hex color code number to a color type +pub fn rgb(hex: u32) -> Rgba { + let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); + Rgba { r, g, b, a: 1.0 } +} + +/// Convert an RGBA hex color code number to [`Rgba`] +pub fn rgba(hex: u32) -> Rgba { + let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); + Rgba { r, g, b, a } +} + +/// Swap from RGBA with premultiplied alpha to BGRA +pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) { + color.swap(0, 2); + if color[3] > 0 { + let a = color[3] as f32 / 255.; + color[0] = (color[0] as f32 / a) as u8; + color[1] = (color[1] as f32 / a) as u8; + color[2] = (color[2] as f32 / a) as u8; + } +} + +/// An RGBA color +#[derive(PartialEq, Clone, Copy, Default)] +#[repr(C)] +pub struct Rgba { + /// The red component of the color, in the range 0.0 to 1.0 + pub r: f32, + /// The green component of the color, in the range 0.0 to 1.0 + pub g: f32, + /// The blue component of the color, in the range 0.0 to 1.0 + pub b: f32, + /// The alpha component of the color, in the range 0.0 to 1.0 + pub a: f32, +} + +impl fmt::Debug for Rgba { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "rgba({:#010x})", u32::from(*self)) + } +} + +impl Rgba { + /// Create a new [`Rgba`] color by blending this and another color together + pub fn blend(&self, other: Rgba) -> Self { + if other.a >= 1.0 { + other + } else if other.a <= 0.0 { + *self + } else { + Rgba { + r: (self.r * (1.0 - other.a)) + (other.r * other.a), + g: (self.g * (1.0 - other.a)) + (other.g * other.a), + b: (self.b * (1.0 - other.a)) + (other.b * other.a), + a: self.a, + } + } + } + + /// Returns a new RGBA color with the same red, green and blue channels, but + /// with a new alpha value. + /// + /// Example: + /// ``` + /// use gpui::rgba; + /// let color = rgba(0xFF0000FF); + /// let faded = color.alpha(0.25); + /// assert_eq!(faded.a, 0.25); + /// ``` + /// + /// This will return a red color with 25% opacity. + /// + /// Example: + /// ``` + /// use gpui::rgba; + /// let color = rgba(0x3399FFCC); + /// let transparent = color.alpha(0.0); + /// assert_eq!(transparent.a, 0.0); + /// ``` + /// + /// This will return the same blue color, fully transparent. + pub fn alpha(&self, a: f32) -> Self { + Rgba { + r: self.r, + g: self.g, + b: self.b, + a: a.clamp(0., 1.), + } + } + + /// Returns a new RGBA color with the same red, green, and blue channels, + /// but with the alpha channel multiplied by the given factor. + /// + /// Example: + /// ``` + /// use gpui::rgba; + /// let color = rgba(0xFF0000FF); // Fully opaque red + /// let faded = color.opacity(0.5); + /// assert_eq!(faded.a, 0.5); + /// ``` + /// + /// This will return a red color with 50% opacity. + /// + /// Example: + /// ``` + /// use gpui::rgba; + /// let color = rgba(0x3399FFCC); // A light blue with 80% opacity + /// let faded = color.opacity(0.5); + /// assert!((faded.a - 0.4).abs() < 1e-6); + /// ``` + /// + /// This will return the same blue color scaled down to 40% opacity. + pub fn opacity(&self, factor: f32) -> Self { + Rgba { + r: self.r, + g: self.g, + b: self.b, + a: self.a * factor.clamp(0., 1.), + } + } +} + +impl From for u32 { + fn from(rgba: Rgba) -> Self { + let r = (rgba.r * 255.0) as u32; + let g = (rgba.g * 255.0) as u32; + let b = (rgba.b * 255.0) as u32; + let a = (rgba.a * 255.0) as u32; + (r << 24) | (g << 16) | (b << 8) | a + } +} + +struct RgbaVisitor; + +impl Visitor<'_> for RgbaVisitor { + type Value = Rgba; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string in the format #rrggbb or #rrggbbaa") + } + + fn visit_str(self, value: &str) -> Result { + Rgba::try_from(value).map_err(E::custom) + } +} + +impl JsonSchema for Rgba { + fn schema_name() -> Cow<'static, str> { + "Rgba".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$" + }) + } +} + +impl<'de> Deserialize<'de> for Rgba { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(RgbaVisitor) + } +} + +impl Serialize for Rgba { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let r = (self.r * 255.0).round() as u8; + let g = (self.g * 255.0).round() as u8; + let b = (self.b * 255.0).round() as u8; + let a = (self.a * 255.0).round() as u8; + + let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}"); + serializer.serialize_str(&s) + } +} + +impl From for Rgba { + fn from(color: Hsla) -> Self { + let h = color.h; + let s = color.s; + let l = color.l; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let cm = c + m; + let xm = x + m; + + let (r, g, b) = match (h * 6.0).floor() as i32 { + 0 | 6 => (cm, xm, m), + 1 => (xm, cm, m), + 2 => (m, cm, xm), + 3 => (m, xm, cm), + 4 => (xm, m, cm), + _ => (cm, m, xm), + }; + + Rgba { + r: r.clamp(0., 1.), + g: g.clamp(0., 1.), + b: b.clamp(0., 1.), + a: color.a, + } + } +} + +impl TryFrom<&'_ str> for Rgba { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + const RGB: usize = "rgb".len(); + const RGBA: usize = "rgba".len(); + const RRGGBB: usize = "rrggbb".len(); + const RRGGBBAA: usize = "rrggbbaa".len(); + + const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; + const INVALID_UNICODE: &str = "invalid unicode characters in color"; + + let Some(("", hex)) = value.trim().split_once('#') else { + bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); + }; + + let (r, g, b, a) = match hex.len() { + RGB | RGBA => { + let r = u8::from_str_radix( + hex.get(0..1).with_context(|| { + format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let g = u8::from_str_radix( + hex.get(1..2).with_context(|| { + format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let b = u8::from_str_radix( + hex.get(2..3).with_context(|| { + format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'") + })?, + 16, + )?; + let a = if hex.len() == RGBA { + u8::from_str_radix( + hex.get(3..4).with_context(|| { + format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'") + })?, + 16, + )? + } else { + 0xf + }; + + /// Duplicates a given hex digit. + /// E.g., `0xf` -> `0xff`. + const fn duplicate(value: u8) -> u8 { + (value << 4) | value + } + + (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) + } + RRGGBB | RRGGBBAA => { + let r = u8::from_str_radix( + hex.get(0..2).with_context(|| { + format!( + "{}: r component of #rrggbb/#rrggbbaa for value: '{}'", + INVALID_UNICODE, value + ) + })?, + 16, + )?; + let g = u8::from_str_radix( + hex.get(2..4).with_context(|| { + format!( + "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'" + ) + })?, + 16, + )?; + let b = u8::from_str_radix( + hex.get(4..6).with_context(|| { + format!( + "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'" + ) + })?, + 16, + )?; + let a = if hex.len() == RRGGBBAA { + u8::from_str_radix( + hex.get(6..8).with_context(|| { + format!( + "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'" + ) + })?, + 16, + )? + } else { + 0xff + }; + (r, g, b, a) + } + _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), + }; + + Ok(Rgba { + r: r as f32 / 255., + g: g as f32 / 255., + b: b as f32 / 255., + a: a as f32 / 255., + }) + } +} + +/// An HSLA color +#[derive(Default, Copy, Clone, Debug)] +#[repr(C)] +pub struct Hsla { + /// Hue, in a range from 0 to 1 + pub h: f32, + + /// Saturation, in a range from 0 to 1 + pub s: f32, + + /// Lightness, in a range from 0 to 1 + pub l: f32, + + /// Alpha, in a range from 0 to 1 + pub a: f32, +} + +#[cfg(feature = "proptest")] +mod property { + use super::Hsla; + use proptest::prelude::*; + + impl Hsla { + /// Proptest [`Strategy`] that produces opaque colors (i.e. alpha = 1). + /// + /// For truly arbitrary colors, use the [`Arbitrary`] implementation. + pub fn opaque_strategy() -> impl Strategy { + (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. }) + } + } + + impl Arbitrary for Hsla { + type Strategy = BoxedStrategy; + type Parameters = (); + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0) + .prop_map(|(h, s, l, a)| Hsla { h, s, l, a }) + .boxed() + } + } +} + +impl PartialEq for Hsla { + fn eq(&self, other: &Self) -> bool { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + .is_eq() + } +} + +impl PartialOrd for Hsla { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Hsla { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.h + .total_cmp(&other.h) + .then(self.s.total_cmp(&other.s)) + .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))) + } +} + +impl Eq for Hsla {} + +impl Hash for Hsla { + fn hash(&self, state: &mut H) { + state.write_u32(u32::from_be_bytes(self.h.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.s.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.l.to_be_bytes())); + state.write_u32(u32::from_be_bytes(self.a.to_be_bytes())); + } +} + +impl Display for Hsla { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", + self.h * 360., + self.s * 100., + self.l * 100., + self.a + ) + } +} + +/// Construct an [`Hsla`] object from plain values +pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { + h: h.clamp(0., 1.), + s: s.clamp(0., 1.), + l: l.clamp(0., 1.), + a: a.clamp(0., 1.), + } +} + +/// Pure black in [`Hsla`] +pub const fn black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 1., + } +} + +/// Transparent black in [`Hsla`] +pub const fn transparent_black() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 0., + a: 0., + } +} + +/// Transparent white in [`Hsla`] +pub const fn transparent_white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 0., + } +} + +/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] +pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { + Hsla { + h: 0., + s: 0., + l: lightness.clamp(0., 1.), + a: opacity.clamp(0., 1.), + } +} + +/// Pure white in [`Hsla`] +pub const fn white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 1., + } +} + +/// The color red in [`Hsla`] +pub const fn red() -> Hsla { + Hsla { + h: 0., + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color blue in [`Hsla`] +pub const fn blue() -> Hsla { + Hsla { + h: 0.6666666667, + s: 1., + l: 0.5, + a: 1., + } +} + +/// The color green in [`Hsla`] +pub const fn green() -> Hsla { + Hsla { + h: 0.3333333333, + s: 1., + l: 0.25, + a: 1., + } +} + +/// The color yellow in [`Hsla`] +pub const fn yellow() -> Hsla { + Hsla { + h: 0.1666666667, + s: 1., + l: 0.5, + a: 1., + } +} + +impl Hsla { + /// Converts this HSLA color to an RGBA color. + pub fn to_rgb(self) -> Rgba { + self.into() + } + + /// The color red + pub const fn red() -> Self { + red() + } + + /// The color green + pub const fn green() -> Self { + green() + } + + /// The color blue + pub const fn blue() -> Self { + blue() + } + + /// The color black + pub const fn black() -> Self { + black() + } + + /// The color white + pub const fn white() -> Self { + white() + } + + /// The color transparent black + pub const fn transparent_black() -> Self { + transparent_black() + } + + /// Returns true if the HSLA color is fully transparent, false otherwise. + pub fn is_transparent(&self) -> bool { + self.a == 0.0 + } + + /// Returns true if the HSLA color is fully opaque, false otherwise. + pub fn is_opaque(&self) -> bool { + self.a == 1.0 + } + + /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors. + /// + /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color. + /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color. + /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values. + /// + /// Assumptions: + /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. + /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. + /// - RGB color components are contained in the range [0, 1]. + /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. + pub fn blend(self, other: Hsla) -> Hsla { + let alpha = other.a; + + if alpha >= 1.0 { + other + } else if alpha <= 0.0 { + self + } else { + let converted_self = Rgba::from(self); + let converted_other = Rgba::from(other); + let blended_rgb = converted_self.blend(converted_other); + Hsla::from(blended_rgb) + } + } + + /// Returns a new HSLA color with the same hue, and lightness, but with no saturation. + pub fn grayscale(&self) -> Self { + Hsla { + h: self.h, + s: 0., + l: self.l, + a: self.a, + } + } + + /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. + /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. + pub fn fade_out(&mut self, factor: f32) { + self.a *= 1.0 - factor.clamp(0., 1.); + } + + /// Multiplies the alpha value of the color by a given factor + /// and returns a new HSLA color. + /// + /// Useful for transforming colors with dynamic opacity, + /// like a color from an external source. + /// + /// Example: + /// ``` + /// let color = gpui::red(); + /// let faded_color = color.opacity(0.5); + /// assert_eq!(faded_color.a, 0.5); + /// ``` + /// + /// This will return a red color with half the opacity. + /// + /// Example: + /// ``` + /// use gpui::hsla; + /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue + /// let faded_color = color.opacity(0.16); + /// assert!((faded_color.a - 0.112).abs() < 1e-6); + /// ``` + /// + /// This will return a blue color with around ~10% opacity, + /// suitable for an element's hover or selected state. + /// + pub fn opacity(&self, factor: f32) -> Self { + Hsla { + h: self.h, + s: self.s, + l: self.l, + a: self.a * factor.clamp(0., 1.), + } + } + + /// Returns a new HSLA color with the same hue, saturation, + /// and lightness, but with a new alpha value. + /// + /// Example: + /// ``` + /// let color = gpui::red(); + /// let red_color = color.alpha(0.25); + /// assert_eq!(red_color.a, 0.25); + /// ``` + /// + /// This will return a red color with 25% opacity. + /// + /// Example: + /// ``` + /// use gpui::hsla; + /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue + /// let faded_color = color.alpha(0.25); + /// assert_eq!(faded_color.a, 0.25); + /// ``` + /// + /// This will return a blue color with 25% opacity. + pub fn alpha(&self, a: f32) -> Self { + Hsla { + h: self.h, + s: self.s, + l: self.l, + a: a.clamp(0., 1.), + } + } +} + +impl From for Hsla { + fn from(color: Rgba) -> Self { + let r = color.r; + let g = color.g; + let b = color.b; + + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + let s = if l == 0.0 || l == 1.0 { + 0.0 + } else if l < 0.5 { + delta / (2.0 * l) + } else { + delta / (2.0 - 2.0 * l) + }; + + let h = if delta == 0.0 { + 0.0 + } else if max == r { + ((g - b) / delta).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / delta + 2.0) / 6.0 + } else { + ((r - g) / delta + 4.0) / 6.0 + }; + + Hsla { + h, + s, + l, + a: color.a, + } + } +} + +impl JsonSchema for Hsla { + fn schema_name() -> Cow<'static, str> { + Rgba::schema_name() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + Rgba::json_schema(generator) + } +} + +impl Serialize for Hsla { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Rgba::from(*self).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Hsla { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Rgba::deserialize(deserializer)?.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub(crate) enum BackgroundTag { + Solid = 0, + LinearGradient = 1, + PatternSlash = 2, + Checkerboard = 3, +} + +/// A color space for color interpolation. +/// +/// References: +/// - +/// - +#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub enum ColorSpace { + #[default] + /// The sRGB color space. + Srgb = 0, + /// The Oklab color space. + Oklab = 1, +} + +impl Display for ColorSpace { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ColorSpace::Srgb => write!(f, "sRGB"), + ColorSpace::Oklab => write!(f, "Oklab"), + } + } +} + +/// A background color, which can be either a solid color or a linear gradient. +#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Background { + pub(crate) tag: BackgroundTag, + pub(crate) color_space: ColorSpace, + pub(crate) solid: Hsla, + pub(crate) gradient_angle_or_pattern_height: f32, + pub(crate) colors: [LinearColorStop; 2], + /// Padding for alignment for repr(C) layout. + pad: u32, +} + +impl std::fmt::Debug for Background { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self.tag { + BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid), + BackgroundTag::LinearGradient => write!( + f, + "LinearGradient({}, {:?}, {:?})", + self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1] + ), + BackgroundTag::PatternSlash => write!( + f, + "PatternSlash({:?}, {})", + self.solid, self.gradient_angle_or_pattern_height + ), + BackgroundTag::Checkerboard => write!( + f, + "Checkerboard({:?}, {})", + self.solid, self.gradient_angle_or_pattern_height + ), + } + } +} + +impl Eq for Background {} +impl Default for Background { + fn default() -> Self { + Self { + tag: BackgroundTag::Solid, + solid: Hsla::default(), + color_space: ColorSpace::default(), + gradient_angle_or_pattern_height: 0.0, + colors: [LinearColorStop::default(), LinearColorStop::default()], + pad: 0, + } + } +} + +/// Creates a hash pattern background +pub fn pattern_slash(color: impl Into, width: f32, interval: f32) -> Background { + let width_scaled = (width * 255.0) as u32; + let interval_scaled = (interval * 255.0) as u32; + let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32; + + Background { + tag: BackgroundTag::PatternSlash, + solid: color.into(), + gradient_angle_or_pattern_height: height, + ..Default::default() + } +} + +/// Creates a checkerboard pattern background +pub fn checkerboard(color: impl Into, size: f32) -> Background { + Background { + tag: BackgroundTag::Checkerboard, + solid: color.into(), + gradient_angle_or_pattern_height: size, + ..Default::default() + } +} + +/// Creates a solid background color. +pub fn solid_background(color: impl Into) -> Background { + Background { + solid: color.into(), + ..Default::default() + } +} + +/// Creates a LinearGradient background color. +/// +/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there. +/// +/// The `angle` is in degrees value in the range 0.0 to 360.0. +/// +/// +pub fn linear_gradient( + angle: f32, + from: impl Into, + to: impl Into, +) -> Background { + Background { + tag: BackgroundTag::LinearGradient, + gradient_angle_or_pattern_height: angle, + colors: [from.into(), to.into()], + ..Default::default() + } +} + +/// A color stop in a linear gradient. +/// +/// +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct LinearColorStop { + /// The color of the color stop. + pub color: Hsla, + /// The percentage of the gradient, in the range 0.0 to 1.0. + pub percentage: f32, +} + +/// Creates a new linear color stop. +/// +/// The percentage of the gradient, in the range 0.0 to 1.0. +pub fn linear_color_stop(color: impl Into, percentage: f32) -> LinearColorStop { + LinearColorStop { + color: color.into(), + percentage, + } +} + +impl LinearColorStop { + /// Returns a new color stop with the same color, but with a modified alpha value. + pub fn opacity(&self, factor: f32) -> Self { + Self { + percentage: self.percentage, + color: self.color.opacity(factor), + } + } +} + +impl Background { + /// Returns the solid color if this is a solid background, None otherwise. + pub fn as_solid(&self) -> Option { + if self.tag == BackgroundTag::Solid { + Some(self.solid) + } else { + None + } + } + + /// Use specified color space for color interpolation. + /// + /// + pub fn color_space(mut self, color_space: ColorSpace) -> Self { + self.color_space = color_space; + self + } + + /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value. + pub fn opacity(&self, factor: f32) -> Self { + let mut background = *self; + background.solid = background.solid.opacity(factor); + background.colors = [ + self.colors[0].opacity(factor), + self.colors[1].opacity(factor), + ]; + background + } + + /// Returns whether the background color is transparent. + pub fn is_transparent(&self) -> bool { + match self.tag { + BackgroundTag::Solid => self.solid.is_transparent(), + BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()), + BackgroundTag::PatternSlash => self.solid.is_transparent(), + BackgroundTag::Checkerboard => self.solid.is_transparent(), + } + } +} + +impl From for Background { + fn from(value: Hsla) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: value, + ..Default::default() + } + } +} + +impl From for Background { + fn from(value: Rgba) -> Self { + Background { + tag: BackgroundTag::Solid, + solid: Hsla::from(value), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_deserialize_three_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_four_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_six_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_padding_to_rgba() { + let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); + + assert_eq!(actual, rgba(0xf5f5f5ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); + + assert_eq!(actual, rgba(0xdeadbeef)) + } + + #[test] + fn test_background_solid() { + let color = Hsla::from(rgba(0xff0099ff)); + let mut background = Background::from(color); + assert_eq!(background.tag, BackgroundTag::Solid); + assert_eq!(background.solid, color); + + assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); + assert!(!background.is_transparent()); + background.solid = hsla(0.0, 0.0, 0.0, 0.0); + assert!(background.is_transparent()); + } + + #[test] + fn test_background_linear_gradient() { + let from = linear_color_stop(rgba(0xff0099ff), 0.0); + let to = linear_color_stop(rgba(0x00ff99ff), 1.0); + let background = linear_gradient(90.0, from, to); + assert_eq!(background.tag, BackgroundTag::LinearGradient); + assert_eq!(background.colors[0], from); + assert_eq!(background.colors[1], to); + + assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5)); + assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5)); + assert!(!background.is_transparent()); + assert!(background.opacity(0.0).is_transparent()); + } + + #[test] + fn test_rgba_alpha() { + let color = Rgba { + r: 0.2, + g: 0.6, + b: 1.0, + a: 0.8, + }; + + assert_eq!(color.alpha(0.25).a, 0.25); + assert_eq!(color.alpha(1.5).a, 1.0); + } + + #[test] + fn test_rgba_opacity() { + let color = Rgba { + r: 0.2, + g: 0.6, + b: 1.0, + a: 0.8, + }; + assert!((color.opacity(0.5).a - 0.4).abs() < 1e-6); + assert_eq!(color.opacity(2.0).a, 0.8); + } +} diff --git a/vendor/gpui-pre/src/colors.rs b/vendor/gpui-pre/src/colors.rs new file mode 100644 index 0000000..ef11ef5 --- /dev/null +++ b/vendor/gpui-pre/src/colors.rs @@ -0,0 +1,122 @@ +use crate::{App, Global, Rgba, Window, WindowAppearance, rgb}; +use std::ops::Deref; +use std::sync::Arc; + +/// The default set of colors for gpui. +/// +/// These are used for styling base components, examples and more. +#[derive(Clone, Debug)] +pub struct Colors { + /// Text color + pub text: Rgba, + /// Selected text color + pub selected_text: Rgba, + /// Background color + pub background: Rgba, + /// Disabled color + pub disabled: Rgba, + /// Selected color + pub selected: Rgba, + /// Border color + pub border: Rgba, + /// Separator color + pub separator: Rgba, + /// Container color + pub container: Rgba, +} + +impl Default for Colors { + fn default() -> Self { + Self::light() + } +} + +impl Colors { + /// Returns the default colors for the given window appearance. + pub fn for_appearance(window: &Window) -> Self { + match window.appearance() { + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(), + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(), + } + } + + /// Returns the default dark colors. + pub fn dark() -> Self { + Self { + text: rgb(0xffffff), + selected_text: rgb(0xffffff), + disabled: rgb(0x565656), + selected: rgb(0x2457ca), + background: rgb(0x222222), + border: rgb(0x000000), + separator: rgb(0xd9d9d9), + container: rgb(0x262626), + } + } + + /// Returns the default light colors. + pub fn light() -> Self { + Self { + text: rgb(0x252525), + selected_text: rgb(0xffffff), + background: rgb(0xffffff), + disabled: rgb(0xb0b0b0), + selected: rgb(0x2a63d9), + border: rgb(0xd9d9d9), + separator: rgb(0xe6e6e6), + container: rgb(0xf4f5f5), + } + } + + /// Get [Colors] from the global state + pub fn get_global(cx: &App) -> &Arc { + &cx.global::().0 + } +} + +/// Get [Colors] from the global state +#[derive(Clone, Debug)] +pub struct GlobalColors(pub Arc); + +impl Deref for GlobalColors { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Global for GlobalColors {} + +/// Implement this trait to allow global [Colors] access via `cx.default_colors()`. +pub trait DefaultColors { + /// Returns the default [`Colors`] + fn default_colors(&self) -> &Arc; +} + +impl DefaultColors for App { + fn default_colors(&self) -> &Arc { + &self.global::().0 + } +} + +/// The appearance of the base GPUI colors, used to style GPUI elements +/// +/// Varies based on the system's current [`WindowAppearance`]. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum DefaultAppearance { + /// Use the set of colors for light appearances. + #[default] + Light, + /// Use the set of colors for dark appearances. + Dark, +} + +impl From for DefaultAppearance { + fn from(appearance: WindowAppearance) -> Self { + match appearance { + WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, + WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, + } + } +} diff --git a/vendor/gpui-pre/src/debug_overlay.rs b/vendor/gpui-pre/src/debug_overlay.rs new file mode 100644 index 0000000..0cca543 --- /dev/null +++ b/vendor/gpui-pre/src/debug_overlay.rs @@ -0,0 +1,428 @@ +//! A developer overlay that paints frame-time statistics directly into the +//! scene, bypassing layout, text, and view invalidation entirely (to avoid +//! infinitely triggering new frames). + +use crate::{ + BorderStyle, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Quad, ScaledPixels, Scene, + Size, point, rgba, size, transparent_black, +}; +use std::{collections::VecDeque, time::Duration}; + +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum DebugFrameOverlayMode { + #[default] + Hidden, + Minimal, + Full, +} + +impl DebugFrameOverlayMode { + /// Returns the next mode in the Hidden → FrameTime → Detailed cycle. + pub fn next(self) -> Self { + match self { + Self::Hidden => Self::Minimal, + Self::Minimal => Self::Full, + Self::Full => Self::Hidden, + } + } +} + +/// The number of most recent draw durations retained for percentile statistics. +const MAX_SAMPLES: usize = 1000; + +const GLYPH_WIDTH: usize = 5; +const GLYPH_HEIGHT: usize = 7; +/// Glyph advance and line advance, in font cells. +const CHAR_ADVANCE: f32 = (GLYPH_WIDTH + 1) as f32; +const LINE_ADVANCE: f32 = (GLYPH_HEIGHT + 2) as f32; +/// Padding between the panel edge and the text, in font cells. +const PANEL_PADDING: f32 = 2.0; +/// Margin between the panel and the window corner, in font cells. +const PANEL_MARGIN: f32 = 4.0; +/// Side of one square font cell, in logical pixels. +const CELL_SIZE: f32 = 2.0; + +fn text_color() -> Hsla { + rgba(0x33ff33ff).into() +} + +fn panel_color() -> Hsla { + rgba(0x000000aa).into() +} + +pub(crate) struct DebugFrameOverlay { + mode: DebugFrameOverlayMode, + draw_durations: VecDeque, + total_frame_count: u64, +} + +impl DebugFrameOverlay { + pub(crate) fn new() -> Self { + Self { + mode: DebugFrameOverlayMode::default(), + draw_durations: VecDeque::new(), + total_frame_count: 0, + } + } + + pub(crate) fn mode(&self) -> DebugFrameOverlayMode { + self.mode + } + + pub(crate) fn set_mode(&mut self, mode: DebugFrameOverlayMode) { + self.mode = mode; + } + + /// Clears the draw-duration samples, restarting the percentile window. + /// The total frame count is left untouched. + pub(crate) fn reset_stats(&mut self) { + self.draw_durations.clear(); + } + + pub(crate) fn is_enabled(&self) -> bool { + self.mode != DebugFrameOverlayMode::Hidden + } + + pub(crate) fn record_frame(&mut self, draw_duration: Duration) { + self.total_frame_count += 1; + if self.draw_durations.len() >= MAX_SAMPLES { + self.draw_durations.pop_front(); + } + self.draw_durations.push_back(draw_duration); + } + + pub(crate) fn paint(&self, scene: &mut Scene, viewport_size: Size, scale_factor: f32) { + if !self.is_enabled() { + return; + } + + let lines = self.lines(); + let max_line_chars = lines.iter().map(|line| line.len()).max().unwrap_or(0); + // Ensure at least one physical pixel per cell so the text stays legible + // at fractional downscale factors. + let cell = (CELL_SIZE * scale_factor).max(1.0); + + let panel_width = cell * (max_line_chars as f32 * CHAR_ADVANCE + 2.0 * PANEL_PADDING); + let panel_height = cell * (lines.len() as f32 * LINE_ADVANCE + 2.0 * PANEL_PADDING); + let viewport = viewport_size.scale(scale_factor); + let panel_left = viewport.width.0 - panel_width - cell * PANEL_MARGIN; + let panel_top = cell * PANEL_MARGIN; + + let content_mask = ContentMask { + bounds: Bounds { + origin: point(ScaledPixels(0.), ScaledPixels(0.)), + size: viewport, + }, + }; + + scene.insert_primitive(solid_quad( + scaled_bounds(panel_left, panel_top, panel_width, panel_height), + &content_mask, + panel_color(), + )); + + let text_color = text_color(); + for (line_index, line) in lines.iter().enumerate() { + let line_top = panel_top + cell * (PANEL_PADDING + line_index as f32 * LINE_ADVANCE); + for (char_index, character) in line.chars().enumerate() { + let Some(rows) = glyph(character) else { + continue; + }; + let glyph_left = + panel_left + cell * (PANEL_PADDING + char_index as f32 * CHAR_ADVANCE); + for (row_index, row) in rows.iter().enumerate() { + let row_top = line_top + cell * row_index as f32; + // Merge horizontal runs of lit cells into single quads. + let mut column = 0; + while column < GLYPH_WIDTH { + if row & (1 << (GLYPH_WIDTH - 1 - column)) == 0 { + column += 1; + continue; + } + let run_start = column; + while column < GLYPH_WIDTH && row & (1 << (GLYPH_WIDTH - 1 - column)) != 0 { + column += 1; + } + scene.insert_primitive(solid_quad( + scaled_bounds( + glyph_left + cell * run_start as f32, + row_top, + cell * (column - run_start) as f32, + cell, + ), + &content_mask, + text_color, + )); + } + } + } + } + } + + fn lines(&self) -> Vec { + let current = self.draw_durations.back().copied(); + match self.mode { + DebugFrameOverlayMode::Hidden => Vec::new(), + DebugFrameOverlayMode::Minimal => vec![format_ms(current)], + DebugFrameOverlayMode::Full => { + let mut sorted: Vec = self.draw_durations.iter().copied().collect(); + sorted.sort_unstable(); + let percentile = |numerator: usize| { + (!sorted.is_empty()).then(|| sorted[(sorted.len() - 1) * numerator / 100]) + }; + // Past five digits the count would break the column + // alignment, so it saturates instead. + let frame_count = if self.total_frame_count > 99_999 { + "LOTS".to_string() + } else { + self.total_frame_count.to_string() + }; + // Labels are padded to a uniform width so the fixed-width + // durations start in the same column on every line. + vec![ + format!("CUR {}", format_ms(current)), + format!("1% {}", format_ms(percentile(99))), + format!("10% {}", format_ms(percentile(90))), + format!("MAX {}", format_ms(sorted.last().copied())), + format!("FRAMES {frame_count:>5}"), + ] + } + } + } +} + +/// Formats as `abc.d MS`, right-aligned in room for three integer digits and +/// one decimal (padded with spaces, not zeroes), so stacked readouts align. +fn format_ms(duration: Option) -> String { + match duration { + Some(duration) => { + let ms = duration.as_secs_f32() * 1000.0; + format!("{ms:>5.1} MS") + } + None => " -- MS".into(), + } +} + +fn scaled_bounds(left: f32, top: f32, width: f32, height: f32) -> Bounds { + Bounds { + origin: point(ScaledPixels(left), ScaledPixels(top)), + size: size(ScaledPixels(width), ScaledPixels(height)), + } +} + +fn solid_quad( + bounds: Bounds, + content_mask: &ContentMask, + color: Hsla, +) -> Quad { + Quad { + order: 0, + border_style: BorderStyle::Solid, + bounds, + content_mask: *content_mask, + background: color.into(), + border_color: transparent_black(), + corner_radii: Corners::default(), + border_widths: Edges::default(), + } +} + +/// Returns the 5x7 bitmap for the given character, one `u8` of column bits per +/// row with the most significant of the 5 bits leftmost. Only the characters +/// used by the overlay's readouts are defined. +fn glyph(character: char) -> Option<[u8; GLYPH_HEIGHT]> { + Some(match character { + '0' => [ + 0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110, + ], + '1' => [ + 0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110, + ], + '2' => [ + 0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111, + ], + '3' => [ + 0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110, + ], + '4' => [ + 0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010, + ], + '5' => [ + 0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110, + ], + '6' => [ + 0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110, + ], + '7' => [ + 0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000, + ], + '8' => [ + 0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110, + ], + '9' => [ + 0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100, + ], + '.' => [ + 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100, + ], + '-' => [ + 0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000, + ], + '%' => [ + 0b11001, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b10011, + ], + 'A' => [ + 0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001, + ], + 'C' => [ + 0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110, + ], + 'E' => [ + 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111, + ], + 'F' => [ + 0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000, + ], + 'L' => [ + 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111, + ], + 'M' => [ + 0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001, + ], + 'N' => [ + 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001, + ], + 'O' => [ + 0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110, + ], + 'R' => [ + 0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001, + ], + 'S' => [ + 0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110, + ], + 'T' => [ + 0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, + ], + 'U' => [ + 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110, + ], + 'X' => [ + 0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001, + ], + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Every character the readouts can produce must have a glyph, or it + /// would silently render as blank space. + #[test] + fn all_rendered_characters_have_glyphs() { + let mut overlay = DebugFrameOverlay::new(); + overlay.set_mode(DebugFrameOverlayMode::Full); + + let mut lines = Vec::new(); + for duration in [ + Duration::ZERO, + Duration::from_micros(1), + Duration::from_micros(8_333), + Duration::from_millis(123), + Duration::from_secs(2), + ] { + overlay.record_frame(duration); + lines.extend(overlay.lines()); + } + // Counts beyond five digits render as "LOTS". + overlay.total_frame_count = 100_000; + lines.extend(overlay.lines()); + + // An enabled overlay with no samples yet renders placeholders. + let mut empty = DebugFrameOverlay::new(); + empty.set_mode(DebugFrameOverlayMode::Full); + lines.extend(empty.lines()); + + for line in lines { + for character in line.chars() { + assert!( + character == ' ' || glyph(character).is_some(), + "no glyph for {character:?} in line {line:?}" + ); + } + } + } + + #[test] + fn percentile_lows_are_reported_as_times() { + let mut overlay = DebugFrameOverlay::new(); + overlay.set_mode(DebugFrameOverlayMode::Full); + for milliseconds in 1..=100 { + overlay.record_frame(Duration::from_millis(milliseconds)); + } + let lines = overlay.lines(); + assert_eq!(lines[0], "CUR 100.0 MS"); + assert_eq!(lines[1], "1% 99.0 MS"); + assert_eq!(lines[2], "10% 90.0 MS"); + assert_eq!(lines[3], "MAX 100.0 MS"); + assert_eq!(lines[4], "FRAMES 100"); + } + + #[test] + fn reset_clears_durations_but_keeps_frame_count() { + let mut overlay = DebugFrameOverlay::new(); + overlay.set_mode(DebugFrameOverlayMode::Full); + for _ in 0..10 { + overlay.record_frame(Duration::from_millis(10)); + } + overlay.reset_stats(); + let lines = overlay.lines(); + assert_eq!(lines[0], "CUR -- MS"); + assert_eq!(lines[3], "MAX -- MS"); + assert_eq!(lines[4], "FRAMES 10"); + overlay.record_frame(Duration::from_millis(20)); + let lines = overlay.lines(); + assert_eq!(lines[0], "CUR 20.0 MS"); + assert_eq!(lines[4], "FRAMES 11"); + } + + #[test] + fn frame_count_accumulates_across_mode_changes() { + let mut overlay = DebugFrameOverlay::new(); + for _ in 0..3 { + overlay.record_frame(Duration::from_millis(10)); + } + overlay.set_mode(DebugFrameOverlayMode::Minimal); + assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]); + overlay.set_mode(DebugFrameOverlayMode::Full); + overlay.record_frame(Duration::from_millis(10)); + assert_eq!(overlay.lines()[4], "FRAMES 4"); + overlay.set_mode(DebugFrameOverlayMode::Hidden); + overlay.record_frame(Duration::from_millis(10)); + overlay.set_mode(DebugFrameOverlayMode::Full); + assert_eq!(overlay.lines()[4], "FRAMES 5"); + } + + #[test] + fn frame_count_is_right_aligned_and_saturates() { + let mut overlay = DebugFrameOverlay::new(); + overlay.set_mode(DebugFrameOverlayMode::Full); + overlay.record_frame(Duration::from_millis(10)); + assert_eq!(overlay.lines()[4], "FRAMES 1"); + overlay.total_frame_count = 99_999; + assert_eq!(overlay.lines()[4], "FRAMES 99999"); + overlay.total_frame_count = 100_000; + assert_eq!(overlay.lines()[4], "FRAMES LOTS"); + } + + #[test] + fn toggling_on_shows_previous_frame_immediately() { + let mut overlay = DebugFrameOverlay::new(); + overlay.record_frame(Duration::from_millis(10)); + overlay.set_mode(DebugFrameOverlayMode::Minimal); + assert_eq!(overlay.lines(), vec![" 10.0 MS".to_string()]); + } +} diff --git a/vendor/gpui-pre/src/element.rs b/vendor/gpui-pre/src/element.rs new file mode 100644 index 0000000..d655625 --- /dev/null +++ b/vendor/gpui-pre/src/element.rs @@ -0,0 +1,813 @@ +//! Elements are the workhorses of GPUI. They are responsible for laying out and painting all of +//! the contents of a window. Elements form a tree and are laid out according to the web layout +//! standards as implemented by [taffy](https://github.com/DioxusLabs/taffy). Most of the time, +//! you won't need to interact with this module or these APIs directly. Elements provide their +//! own APIs and GPUI, or other element implementation, uses the APIs in this module to convert +//! that element tree into the pixels you see on the screen. +//! +//! # Element Basics +//! +//! Elements are constructed by calling [`Render::render()`] on the root view of the window, +//! which recursively constructs the element tree from the current state of the application,. +//! These elements are then laid out by Taffy, and painted to the screen according to their own +//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element +//! tree and any callbacks they have registered with GPUI are dropped and the process repeats. +//! +//! But some state is too simple and voluminous to store in every view that needs it, e.g. +//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type. +//! +//! # Implementing your own elements +//! +//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding, +//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected +//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`], +//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays +//! and popups and anything else that shows up 'on top' of other elements. +//! With great power, comes great responsibility. +//! +//! However, most of the time, you won't need to implement your own elements. GPUI provides a number of +//! elements that should cover most common use cases out of the box and it's recommended that you use those +//! to construct `components`, using the [`RenderOnce`] trait and the `#[derive(IntoElement)]` macro. Only implement +//! elements when you need to take manual control of the layout and painting process, such as when using +//! your own custom layout algorithm or rendering a code editor. + +#[cfg(any(feature = "inspector", debug_assertions))] +use crate::InspectorElementPath; +use crate::{ + A11ySubtreeBuilder, App, ArenaBox, AvailableSpace, Bounds, Context, DispatchNodeId, ElementId, + FocusHandle, InspectorElementId, LayoutId, Pixels, Point, Size, Style, Window, + util::FluentBuilder, window::with_element_arena, +}; +use derive_more::{Deref, DerefMut}; +use std::{ + any::Any, + fmt::{self, Debug, Display}, + mem, panic, + sync::Arc, +}; + +/// Implemented by types that participate in laying out and painting the contents of a window. +/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. +/// You can create custom elements by implementing this trait, see the module-level documentation +/// for more details. +pub trait Element: 'static + IntoElement { + /// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::prepaint`] and [`Element::paint`]. + type RequestLayoutState: 'static; + + /// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently + /// provided to [`Element::paint`]. + type PrepaintState: 'static; + + /// If this element has a unique identifier, return it here. This is used to track elements across frames, and + /// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods. + /// + /// The global id can in turn be used to access state that's connected to an element with the same id across + /// frames. This id must be unique among children of the first containing element with an id. + fn id(&self) -> Option; + + /// Source location where this element was constructed, used to disambiguate elements in the + /// inspector and navigate to their source code. + fn source_location(&self) -> Option<&'static panic::Location<'static>>; + + /// Before an element can be painted, we need to know where it's going to be and how big it is. + /// Use this method to request a layout from Taffy and initialize the element's state. + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState); + + /// After laying out an element, we need to commit its bounds to the current frame for hitbox + /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState; + + /// Once layout has been completed, this method will be called to paint the element to the screen. + /// The state argument is the same state that was returned from [`Element::request_layout()`]. + fn paint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ); + + /// Returns the accessible role for this element, if any. + /// Elements that return `None` are not included in the accessibility tree. + /// + /// Note: inclusion in accessibility tree requires non-`None` [`id`][Element::id]. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + fn a11y_role(&self) -> Option { + None + } + + /// Write accessibility properties to the given node. + /// Called only when `a11y_role()` returns `Some`. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + fn write_a11y_info(&self, _node: &mut accesskit::Node) {} + + /// Add synthetic child nodes to an [`Element`] that has an + /// [`.id()`][Element::id] and a [`.role()`][Element::a11y_role]. + /// + /// Some elements may want to inject accessibility nodes that do not + /// correspond to any GPUI element. For example, a custom text field element + /// may want to inject synthetic child nodes for the text content. + /// + /// See [Synthetic children](crate::_accessibility#synthetic-children) in + /// the accessibility guide for more detail. + fn a11y_synthetic_children( + &mut self, + _prepaint: &mut Self::PrepaintState, + _builder: &mut A11ySubtreeBuilder, + ) { + } + + /// Convert this element into a dynamically-typed [`AnyElement`]. + fn into_any(self) -> AnyElement { + AnyElement::new(self) + } +} + +/// Implemented by any type that can be converted into an element. +pub trait IntoElement: Sized { + /// The specific type of element into which the implementing type is converted. + /// Useful for converting other types into elements automatically, like Strings + type Element: Element; + + /// Convert self into a type that implements [`Element`]. + fn into_element(self) -> Self::Element; + + /// Convert self into a dynamically-typed [`AnyElement`]. + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } +} + +impl FluentBuilder for T {} + +/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from +/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen. +pub trait Render: 'static + Sized { + /// Render this view into an element tree. + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement; +} + +impl Render for Empty { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } +} + +/// You can derive [`IntoElement`] on any type that implements this trait. +/// It is used to construct reusable `components` out of plain data. Think of +/// components as a recipe for a certain pattern of elements. RenderOnce allows +/// you to invoke this pattern, without breaking the fluent builder pattern of +/// the element APIs. +pub trait RenderOnce: 'static { + /// Render this component into an element tree. Note that this method + /// takes ownership of self, as compared to [`Render::render()`] method + /// which takes a mutable reference. + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; +} + +/// This is a helper trait to provide a uniform interface for constructing elements that +/// can accept any number of any kind of child elements +pub trait ParentElement { + /// Extend this element's children with the given child elements. + fn extend(&mut self, elements: impl IntoIterator); + + /// Add a single child element to this element. + fn child(mut self, child: impl IntoElement) -> Self + where + Self: Sized, + { + self.extend(std::iter::once(child.into_any_element())); + self + } + + /// Add multiple child elements to this element. + fn children(mut self, children: impl IntoIterator) -> Self + where + Self: Sized, + { + self.extend(children.into_iter().map(|child| child.into_any_element())); + self + } +} + +/// A globally unique identifier for an element, used to track state across frames. +#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)] +pub struct GlobalElementId(pub(crate) Arc<[ElementId]>); + +impl Display for GlobalElementId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, element_id) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ".")?; + } + write!(f, "{}", element_id)?; + } + Ok(()) + } +} + +impl GlobalElementId { + pub(crate) fn accesskit_node_id(&self) -> accesskit::NodeId { + use std::hash::{Hash, Hasher}; + let mut hasher = std::hash::DefaultHasher::default(); + self.hash(&mut hasher); + accesskit::NodeId(hasher.finish()) + } +} + +trait ElementObject { + fn inner_element(&mut self) -> &mut dyn Any; + + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId; + + fn prepaint(&mut self, window: &mut Window, cx: &mut App); + + fn paint(&mut self, window: &mut Window, cx: &mut App); + + fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size; +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +pub struct Drawable { + /// The drawn element. + pub element: E, + phase: ElementDrawPhase, +} + +#[derive(Default)] +enum ElementDrawPhase { + #[default] + Start, + RequestLayout { + layout_id: LayoutId, + global_id: Option, + inspector_id: Option, + request_layout: RequestLayoutState, + }, + LayoutComputed { + layout_id: LayoutId, + global_id: Option, + inspector_id: Option, + available_space: Size, + request_layout: RequestLayoutState, + }, + Prepaint { + node_id: DispatchNodeId, + global_id: Option, + inspector_id: Option, + bounds: Bounds, + request_layout: RequestLayoutState, + prepaint: PrepaintState, + }, + Painted, +} + +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. +impl Drawable { + pub(crate) fn new(element: E) -> Self { + Drawable { + element, + phase: ElementDrawPhase::Start, + } + } + + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + match mem::take(&mut self.phase) { + ElementDrawPhase::Start => { + let global_id = self + .element + .id() + .map(|element_id| prepare_element_id(element_id, window)); + + let inspector_id; + #[cfg(any(feature = "inspector", debug_assertions))] + { + inspector_id = if window.inspector_enabled() { + self.element + .source_location() + .map(|source| prepare_inspector_id(source, window)) + } else { + None + }; + } + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + inspector_id = None; + } + + let (layout_id, request_layout) = self.element.request_layout( + global_id.as_ref(), + inspector_id.as_ref(), + window, + cx, + ); + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + request_layout, + }; + layout_id + } + _ => panic!("must call request_layout only once"), + } + } + + pub(crate) fn prepaint(&mut self, window: &mut Window, cx: &mut App) { + match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + mut request_layout, + } + | ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + mut request_layout, + .. + } => { + if let Some(element_id) = self.element.id() { + window.element_id_stack.push(element_id); + debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); + } + + let bounds = window.layout_bounds(layout_id); + let mut pushed_a11y_node = false; + if window.a11y.is_active() { + if let Some(global_id) = global_id.as_ref() { + if let Some(role) = self.element.a11y_role() { + let node_id = global_id.accesskit_node_id(); + let mut node = accesskit::Node::new(role); + let scale = window.scale_factor(); + node.set_bounds(accesskit::Rect { + x0: (bounds.origin.x.0 * scale) as f64, + y0: (bounds.origin.y.0 * scale) as f64, + x1: ((bounds.origin.x.0 + bounds.size.width.0) * scale) as f64, + y1: ((bounds.origin.y.0 + bounds.size.height.0) * scale) as f64, + }); + self.element.write_a11y_info(&mut node); + window.a11y.node_bounds.insert(node_id, bounds); + pushed_a11y_node = window.a11y.nodes.push(node_id, node); + #[cfg(debug_assertions)] + if pushed_a11y_node { + let view = window + .a11y + .view_type_names + .get(&window.current_view()) + .copied(); + let source_location = self.element.source_location(); + window.a11y.nodes.record_node_info( + node_id, + crate::window::a11y::debug::NodeDebugInfo { + synthetic: false, + view, + element_id: global_id.0.last().map(|id| format!("{id:?}")), + source_location, + }, + ); + } + } + } + } + + let node_id = window.next_frame.dispatch_tree.push_node(); + let mut prepaint = self.element.prepaint( + global_id.as_ref(), + inspector_id.as_ref(), + bounds, + &mut request_layout, + window, + cx, + ); + window.next_frame.dispatch_tree.pop_node(); + + if pushed_a11y_node { + if let Some(global_id) = global_id.as_ref() { + #[cfg(debug_assertions)] + let creator = crate::window::a11y::debug::NodeCreator { + view: window + .a11y + .view_type_names + .get(&window.current_view()) + .copied(), + element_id: global_id.0.last().map(|id| format!("{id:?}")), + source_location: self.element.source_location(), + }; + let mut builder = A11ySubtreeBuilder::new( + global_id.accesskit_node_id(), + &mut window.a11y.nodes, + ); + #[cfg(debug_assertions)] + { + builder = builder.with_creator(creator); + } + self.element + .a11y_synthetic_children(&mut prepaint, &mut builder); + } + window.a11y.nodes.pop(); + } + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Prepaint { + node_id, + global_id, + inspector_id, + bounds, + request_layout, + prepaint, + }; + } + _ => panic!("must call request_layout before prepaint"), + } + } + + pub(crate) fn paint( + &mut self, + window: &mut Window, + cx: &mut App, + ) -> (E::RequestLayoutState, E::PrepaintState) { + match mem::take(&mut self.phase) { + ElementDrawPhase::Prepaint { + node_id, + global_id, + inspector_id, + bounds, + mut request_layout, + mut prepaint, + .. + } => { + if let Some(element_id) = self.element.id() { + window.element_id_stack.push(element_id); + debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); + } + + window.next_frame.dispatch_tree.set_active_node(node_id); + self.element.paint( + global_id.as_ref(), + inspector_id.as_ref(), + bounds, + &mut request_layout, + &mut prepaint, + window, + cx, + ); + + if global_id.is_some() { + window.element_id_stack.pop(); + } + + self.phase = ElementDrawPhase::Painted; + (request_layout, prepaint) + } + _ => panic!("must call prepaint before paint"), + } + } + + pub(crate) fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + if matches!(&self.phase, ElementDrawPhase::Start) { + self.request_layout(window, cx); + } + + let layout_id = match mem::take(&mut self.phase) { + ElementDrawPhase::RequestLayout { + layout_id, + global_id, + inspector_id, + request_layout, + } => { + window.compute_layout(layout_id, available_space, cx); + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space, + request_layout, + }; + layout_id + } + ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space: prev_available_space, + request_layout, + } => { + if available_space != prev_available_space { + window.compute_layout(layout_id, available_space, cx); + } + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + global_id, + inspector_id, + available_space, + request_layout, + }; + layout_id + } + _ => panic!("cannot measure after painting"), + }; + + window.layout_bounds(layout_id).size + } +} + +impl ElementObject for Drawable +where + E: Element, + E::RequestLayoutState: 'static, +{ + fn inner_element(&mut self) -> &mut dyn Any { + &mut self.element + } + + #[inline] + fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + Drawable::request_layout(self, window, cx) + } + + #[inline] + fn prepaint(&mut self, window: &mut Window, cx: &mut App) { + Drawable::prepaint(self, window, cx); + } + + #[inline] + fn paint(&mut self, window: &mut Window, cx: &mut App) { + Drawable::paint(self, window, cx); + } + + #[inline] + fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + Drawable::layout_as_root(self, available_space, window, cx) + } +} + +/// A dynamically typed element that can be used to store any element type. +pub struct AnyElement(ArenaBox); + +impl AnyElement { + pub(crate) fn new(element: E) -> Self + where + E: 'static + Element, + E::RequestLayoutState: Any, + { + let element = with_element_arena(|arena| arena.alloc(|| Drawable::new(element))) + .map(|element| element as &mut dyn ElementObject); + AnyElement(element) + } + + /// Attempt to downcast a reference to the boxed element to a specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.0.inner_element().downcast_mut::() + } + + /// Request the layout ID of the element stored in this `AnyElement`. + /// Used for laying out child elements in a parent element. + pub fn request_layout(&mut self, window: &mut Window, cx: &mut App) -> LayoutId { + self.0.request_layout(window, cx) + } + + /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and + /// request autoscroll before the final paint pass is confirmed. + pub fn prepaint(&mut self, window: &mut Window, cx: &mut App) -> Option { + let focus_assigned = window.next_frame.focus.is_some(); + + self.0.prepaint(window, cx); + + if !focus_assigned && let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); + } + + None + } + + /// Paints the element stored in this `AnyElement`. + pub fn paint(&mut self, window: &mut Window, cx: &mut App) { + self.0.paint(window, cx); + } + + /// Performs layout for this element within the given available space and returns its size. + pub fn layout_as_root( + &mut self, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Size { + self.0.layout_as_root(available_space, window, cx) + } + + /// Prepaints this element at the given absolute origin. + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. + pub fn prepaint_at( + &mut self, + origin: Point, + window: &mut Window, + cx: &mut App, + ) -> Option { + window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) + } + + /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. + pub fn prepaint_as_root( + &mut self, + origin: Point, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.layout_as_root(available_space, window, cx); + window.with_absolute_element_offset(origin, |window| self.prepaint(window, cx)) + } +} + +impl Element for AnyElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.request_layout(window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + self.prepaint(window, cx); + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.paint(window, cx); + } +} + +impl IntoElement for AnyElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + + fn into_any_element(self) -> AnyElement { + self + } +} + +/// The empty element, which renders nothing. +pub struct Empty; + +impl IntoElement for Empty { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Empty { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + ( + window.request_layout( + Style { + display: crate::Display::None, + ..Default::default() + }, + None, + cx, + ), + (), + ) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _state: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _window: &mut Window, + _cx: &mut App, + ) { + } +} + +#[inline(never)] +fn prepare_element_id(element_id: ElementId, window: &mut Window) -> GlobalElementId { + window.element_id_stack.push(element_id); + GlobalElementId(Arc::from(&*window.element_id_stack)) +} + +#[cfg(any(feature = "inspector", debug_assertions))] +#[inline(never)] +fn prepare_inspector_id( + source: &'static panic::Location<'static>, + window: &mut Window, +) -> InspectorElementId { + let path = InspectorElementPath { + global_id: GlobalElementId(Arc::from(&*window.element_id_stack)), + source_location: source, + }; + window.build_inspector_element_id(path) +} diff --git a/vendor/gpui-pre/src/elements/anchored.rs b/vendor/gpui-pre/src/elements/anchored.rs new file mode 100644 index 0000000..2926006 --- /dev/null +++ b/vendor/gpui-pre/src/elements/anchored.rs @@ -0,0 +1,398 @@ +use smallvec::SmallVec; + +use crate::{ + Anchor, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style, + Window, point, px, +}; + +/// The state that the anchored element element uses to track its children. +pub struct AnchoredState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +/// An anchored element that can be used to display UI that +/// will avoid overflowing the window bounds. +pub struct Anchored { + children: SmallVec<[AnyElement; 2]>, + anchor: Anchor, + fit_mode: AnchoredFitMode, + anchor_position: Option>, + position_mode: AnchoredPositionMode, + offset: Option>, +} + +/// anchored gives you an element that will avoid overflowing the window bounds. +/// Its children should have no margin to avoid measurement issues. +pub fn anchored() -> Anchored { + Anchored { + children: SmallVec::new(), + anchor: Anchor::TopLeft, + fit_mode: AnchoredFitMode::SwitchAnchor, + anchor_position: None, + position_mode: AnchoredPositionMode::Window, + offset: None, + } +} + +impl Anchored { + /// Sets which corner of the anchored element should be anchored to the current position. + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + + /// Sets the position in window coordinates + /// (otherwise the location the anchored element is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + + /// Offset the final position by this amount. + /// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu. + pub fn offset(mut self, offset: Point) -> Self { + self.offset = Some(offset); + self + } + + /// Sets the position mode for this anchored element. Local will have this + /// interpret its [`Anchored::position`] as relative to the parent element. + /// While Window will have it interpret the position as relative to the window. + pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self { + self.position_mode = mode; + self + } + + /// Snap to window edge instead of switching anchor corner when an overflow would occur. + pub fn snap_to_window(mut self) -> Self { + self.fit_mode = AnchoredFitMode::SnapToWindow; + self + } + + /// Snap to window edge and leave some margins. + pub fn snap_to_window_with_margin(mut self, edges: impl Into>) -> Self { + self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into()); + self + } +} + +impl ParentElement for Anchored { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Element for Anchored { + type RequestLayoutState = AnchoredState; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + + let anchored_style = Style { + position: Position::Absolute, + display: Display::Flex, + ..Style::default() + }; + + let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx); + + (layout_id, AnchoredState { child_layout_ids }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) { + if request_layout.child_layout_ids.is_empty() { + return; + } + + let children_bounds = request_layout + .child_layout_ids + .iter() + .map(|id| window.layout_bounds(*id)) + .reduce(|acc, bounds| acc.union(&bounds)) + .unwrap(); + + let (origin, mut desired) = self.position_mode.get_position_and_bounds( + self.anchor_position, + self.anchor, + children_bounds.size, + bounds, + self.offset, + ); + + let limits = Bounds { + origin: Point::default(), + size: window.viewport_size(), + }; + + if self.fit_mode == AnchoredFitMode::SwitchAnchor { + let mut anchor = self.anchor; + + if desired.left() < limits.left() || desired.right() > limits.right() { + let switched = Bounds::from_anchor_and_size( + anchor.other_side_along(Axis::Horizontal), + origin, + children_bounds.size, + ); + if !(switched.left() < limits.left() || switched.right() > limits.right()) { + anchor = anchor.other_side_along(Axis::Horizontal); + desired = switched + } + } + + if desired.top() < limits.top() || desired.bottom() > limits.bottom() { + let switched = Bounds::from_anchor_and_size( + anchor.other_side_along(Axis::Vertical), + origin, + children_bounds.size, + ); + if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) { + desired = switched; + } + } + } + + let client_inset = window.client_inset.unwrap_or(px(0.)); + let edges = match self.fit_mode { + AnchoredFitMode::SnapToWindowWithMargin(edges) => edges, + _ => Edges::default(), + } + .map(|edge| *edge + client_inset); + + // Snap the horizontal edges of the anchored element to the horizontal edges of the window if + // its horizontal bounds overflow, aligning to the left if it is wider than the limits. + if desired.right() > limits.right() { + desired.origin.x -= desired.right() - limits.right() + edges.right; + } + if desired.left() < limits.left() { + desired.origin.x = limits.origin.x + edges.left; + } + + // Snap the vertical edges of the anchored element to the vertical edges of the window if + // its vertical bounds overflow, aligning to the top if it is taller than the limits. + if desired.bottom() > limits.bottom() { + desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom; + } + if desired.top() < limits.top() { + desired.origin.y = limits.origin.y + edges.top; + } + + let offset = desired.origin - bounds.origin; + let offset = point(offset.x.round(), offset.y.round()); + + window.with_element_offset(offset, |window| { + for child in &mut self.children { + child.prepaint(window, cx); + } + }) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + for child in &mut self.children { + child.paint(window, cx); + } + } +} + +impl IntoElement for Anchored { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Which algorithm to use when fitting the anchored element to be inside the window. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredFitMode { + /// Snap the anchored element to the window edge. + SnapToWindow, + /// Snap to window edge and leave some margins. + SnapToWindowWithMargin(Edges), + /// Switch which corner anchor this anchored element is attached to. + SwitchAnchor, +} + +/// Which algorithm to use when positioning the anchored element. +#[derive(Copy, Clone, PartialEq)] +pub enum AnchoredPositionMode { + /// Position the anchored element relative to the window. + Window, + /// Position the anchored element relative to its parent. + Local, +} + +impl AnchoredPositionMode { + fn get_position_and_bounds( + &self, + anchor_position: Option>, + anchor: Anchor, + size: Size, + bounds: Bounds, + offset: Option>, + ) -> (Point, Bounds) { + let offset = offset.unwrap_or_default(); + + match self { + AnchoredPositionMode::Window => { + let anchor_position = anchor_position.unwrap_or(bounds.origin); + let bounds = Bounds::from_anchor_and_size(anchor, anchor_position + offset, size); + (anchor_position, bounds) + } + AnchoredPositionMode::Local => { + let anchor_position = anchor_position.unwrap_or_default(); + let bounds = Bounds::from_anchor_and_size( + anchor, + bounds.origin + anchor_position + offset, + size, + ); + (anchor_position, bounds) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Context, Pixels, PlatformInput, Point, TestAppContext, Window, deferred, div, point, + prelude::*, px, size, + }; + + struct AnchoredTestView { + position: Point, + } + + impl Render for AnchoredTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full().child( + div() + .id("scroll-container") + .overflow_y_scroll() + .size_full() + .child(div().h(px(2000.)).w_full()) + .child( + deferred( + super::anchored() + .snap_to_window() + .position(self.position) + .child( + div() + .id("menu") + .debug_selector(|| "MENU".into()) + .w(px(200.)) + .h(px(300.)), + ), + ) + .with_priority(1), + ), + ) + } + } + + #[gpui::test] + fn test_anchored_position_without_scroll(cx: &mut TestAppContext) { + let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { + position: point(px(100.), px(100.)), + }); + + cx.run_until_parked(); + + let menu_bounds = window + .update(cx, |_, window, _| { + window.rendered_frame.debug_bounds.get("MENU").copied() + }) + .unwrap() + .expect("MENU debug bounds not found"); + + assert_eq!(menu_bounds.origin, point(px(100.), px(100.))); + assert_eq!(menu_bounds.size, size(px(200.), px(300.))); + } + + #[gpui::test] + fn test_anchored_position_when_scrolled(cx: &mut TestAppContext) { + let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { + position: point(px(100.), px(100.)), + }); + + cx.run_until_parked(); + + window + .update(cx, |_, window, cx| { + let event = gpui::ScrollWheelEvent { + position: point(px(400.), px(300.)), + delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-1000.))), + ..Default::default() + }; + window.dispatch_event(PlatformInput::ScrollWheel(event), cx); + }) + .unwrap(); + + cx.run_until_parked(); + + let menu_bounds = window + .update(cx, |_, window, _| { + window.rendered_frame.debug_bounds.get("MENU").copied() + }) + .unwrap() + .expect("MENU debug bounds not found"); + + assert_eq!(menu_bounds.origin, point(px(100.), px(100.))); + assert_eq!(menu_bounds.size, size(px(200.), px(300.))); + } + + #[gpui::test] + fn test_anchored_snaps_to_window(cx: &mut TestAppContext) { + let window = cx.open_window(size(px(800.), px(600.)), |_, _| AnchoredTestView { + position: point(px(100.), px(500.)), + }); + + cx.run_until_parked(); + + let menu_bounds = window + .update(cx, |_, window, _| { + window.rendered_frame.debug_bounds.get("MENU").copied() + }) + .unwrap() + .expect("MENU debug bounds not found"); + + assert_eq!(menu_bounds.origin, point(px(100.), px(300.))); + assert_eq!(menu_bounds.size, size(px(200.), px(300.))); + } +} diff --git a/vendor/gpui-pre/src/elements/animation.rs b/vendor/gpui-pre/src/elements/animation.rs new file mode 100644 index 0000000..e076893 --- /dev/null +++ b/vendor/gpui-pre/src/elements/animation.rs @@ -0,0 +1,1028 @@ +use scheduler::Instant; +use std::{cell::Cell, rc::Rc, time::Duration}; + +use crate::{ + AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, + ParentElement, SpringAnimation, SpringConfig, SpringPlayback, SpringState, SpringTarget, + Window, +}; + +pub use easing::*; +use smallvec::SmallVec; + +/// An animation that can be applied to an element. +#[derive(Clone)] +pub struct Animation { + /// The amount of time for which this animation should run + pub duration: Duration, + /// Whether to repeat this animation when it finishes + pub oneshot: bool, + /// Whether to derive the phase from a shared clock. See [`Animation::repeat_synced`]. + pub synced: bool, + /// A function that maps normalized time to an animated value. + /// The result may exceed 0..1 for easing functions that overshoot. + pub easing: Rc f32>, + /// The maximum number of times per second this animation re-renders. + /// When `None`, the animation re-renders on every frame. + pub max_fps: Option, +} + +impl Animation { + /// Create a new animation with the given duration. + /// By default the animation will only run once and will use a linear easing function. + pub fn new(duration: Duration) -> Self { + Self { + duration, + oneshot: true, + synced: false, + easing: Rc::new(linear), + max_fps: None, + } + } + + /// Set the animation to loop when it finishes. + pub fn repeat(mut self) -> Self { + self.oneshot = false; + self + } + + /// Set the animation to loop when it finishes, phase-locked to a clock shared by the whole [`App`]. + pub fn repeat_synced(mut self) -> Self { + self.oneshot = false; + self.synced = true; + self + } + + /// Sets the easing function used to map normalized time to an animated value. + /// + /// The output is not clamped, allowing physical easing functions such as + /// springs to overshoot. + pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { + self.easing = Rc::new(easing); + self + } + + /// Limit how often this animation re-renders. Instead of re-rendering on + /// every frame, the animation schedules its next render `1 / max_fps` + /// seconds after the current one. Values that are not finite and positive + /// are ignored. + pub fn with_max_fps(mut self, max_fps: f32) -> Self { + self.max_fps = Some(max_fps); + self + } +} + +/// An extension trait for adding the animation wrapper to both Elements and Components +/// +/// Animations rendered through this trait automatically respect +/// [`App::reduce_motion`](crate::App::reduce_motion): when it is set, +/// the element is rendered in a static state (the end state for oneshot +/// animations, the start state for repeating ones) and no animation frames are +/// scheduled. +pub trait AnimationExt { + /// Render this component or element with an animation + fn with_animation( + self, + id: impl Into, + animation: Animation, + animator: impl Fn(Self, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(move |this, _, value| animator(this, value)), + animations: smallvec::smallvec![animation], + } + } + + /// Render this component or element with a chain of animations + fn with_animations( + self, + id: impl Into, + animations: Vec, + animator: impl Fn(Self, usize, f32) -> Self + 'static, + ) -> AnimationElement + where + Self: Sized, + { + AnimationElement { + id: id.into(), + element: Some(self), + animator: Box::new(animator), + animations: animations.into(), + } + } + + /// Renders this component or element at the value produced by a spring. + /// + /// The element ID preserves position and velocity across target changes. + /// A newly mounted spring starts at its target unless configured with + /// [`SpringAnimation::from`]. + fn with_spring( + self, + id: impl Into, + animation: SpringAnimation, + animator: impl FnOnce(Self, T::Output) -> Self + 'static, + ) -> SpringAnimationElement + where + Self: Sized, + T: SpringTarget, + T::Output: 'static, + { + let SpringAnimation { + config, + target, + epsilon, + initial, + playback, + } = animation; + let scalar_target = target.target(); + SpringAnimationElement { + id: id.into(), + element: Some(self), + config, + target: scalar_target, + epsilon, + initial, + playback, + animator: Some(Box::new(move |this, value| { + animator(this, target.resolve(value)) + })), + } + } +} + +impl AnimationExt for E {} + +/// A GPUI element that applies an animation to another element +pub struct AnimationElement { + id: ElementId, + element: Option, + animations: SmallVec<[Animation; 1]>, + animator: Box E + 'static>, +} + +/// A GPUI element driven by a stateful spring. +pub struct SpringAnimationElement { + id: ElementId, + element: Option, + config: SpringConfig, + target: f32, + epsilon: f32, + initial: Option, + playback: SpringPlayback, + animator: Option E + 'static>>, +} + +impl ParentElement for SpringAnimationElement { + fn extend(&mut self, elements: impl IntoIterator) { + let Some(element) = &mut self.element else { + return; + }; + + element.extend(elements); + } +} + +impl SpringAnimationElement { + /// Returns a new [`SpringAnimationElement`] after applying the given function + /// to the element being animated. + pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> SpringAnimationElement { + self.element = self.element.map(f); + self + } +} + +impl IntoElement for SpringAnimationElement { + type Element = SpringAnimationElement; + + fn into_element(self) -> Self::Element { + self + } +} + +impl ParentElement for AnimationElement { + fn extend(&mut self, elements: impl IntoIterator) { + let Some(element) = &mut self.element else { + return; + }; + + element.extend(elements); + } +} + +impl AnimationElement { + /// Returns a new [`AnimationElement`] after applying the given function + /// to the element being animated. + pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement { + self.element = self.element.map(f); + self + } +} + +impl IntoElement for AnimationElement { + type Element = AnimationElement; + + fn into_element(self) -> Self::Element { + self + } +} + +struct AnimationState { + start: Instant, + animation_ix: usize, + /// Whether a throttled re-render (see [`Animation::with_max_fps`]) is + /// already scheduled, so overlapping renders don't stack extra timers. + delayed_frame_pending: Rc>, +} + +struct SpringElementState { + spring: SpringState, + target: f32, + config: SpringConfig, + initial: f32, + playback: SpringPlayback, + updated_at: Instant, +} + +impl Element for SpringAnimationElement { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + window.with_element_state(global_id.unwrap(), |state, window| { + let now = Instant::now(); + let initial = self.initial.unwrap_or(self.target); + let mut state = state.unwrap_or_else(|| SpringElementState { + spring: SpringState { + position: initial, + velocity: 0.0, + }, + target: self.target, + config: self.config, + initial, + playback: self.playback, + updated_at: now, + }); + + let elapsed = now.duration_since(state.updated_at).as_secs_f32(); + match state.playback { + SpringPlayback::Running => { + state.spring = state.config.step(state.spring, state.target, elapsed); + } + SpringPlayback::Paused + | SpringPlayback::Stopped + | SpringPlayback::Completed + | SpringPlayback::Cancelled => {} + } + + state.config = self.config; + state.target = self.target; + + let done = match self.playback { + SpringPlayback::Running => { + if cx.reduce_motion() { + state.spring = SpringState { + position: state.target, + velocity: 0.0, + }; + true + } else { + let done = + state + .config + .is_settled(state.spring, state.target, self.epsilon); + if done { + state.spring = SpringState { + position: state.target, + velocity: 0.0, + }; + } + done + } + } + SpringPlayback::Paused => true, + SpringPlayback::Stopped => { + state.spring.velocity = 0.0; + true + } + SpringPlayback::Completed => { + state.spring = SpringState { + position: state.target, + velocity: 0.0, + }; + true + } + SpringPlayback::Cancelled => { + state.spring = SpringState { + position: state.initial, + velocity: 0.0, + }; + true + } + }; + state.playback = self.playback; + state.updated_at = now; + + let element = self.element.take().expect("should only be called once"); + let animator = self.animator.take().expect("should only be called once"); + let mut element = animator(element, state.spring.position).into_any_element(); + + if !done { + window.request_animation_frame(); + } + + ((element.request_layout(window, cx), element), state) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + element.prepaint(window, cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + element.paint(window, cx); + } +} + +impl Element for AnimationElement { + type RequestLayoutState = AnyElement; + type PrepaintState = (); + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + window.with_element_state(global_id.unwrap(), |state, window| { + let mut state = state.unwrap_or_else(|| AnimationState { + start: Instant::now(), + animation_ix: 0, + delayed_frame_pending: Rc::new(Cell::new(false)), + }); + let (animation_ix, delta, done) = if cx.reduce_motion() { + let animation_ix = self.animations.len() - 1; + let delta = if self.animations[animation_ix].oneshot { + 1.0 + } else { + 0.0 + }; + (animation_ix, delta, true) + } else { + let animation_ix = state.animation_ix; + let duration = self.animations[animation_ix].duration; + + let elapsed = if self.animations[animation_ix].synced && !duration.is_zero() { + let elapsed = cx.background_executor().now() - cx.synced_animation_epoch; + // Reduce modulo the duration before f32 conversion, which loses sub-second precision at scale. + Duration::from_nanos((elapsed.as_nanos() % duration.as_nanos()) as u64) + } else { + state.start.elapsed() + }; + let mut delta = elapsed.as_secs_f32() / duration.as_secs_f32(); + + let mut done = false; + if delta > 1.0 { + if self.animations[animation_ix].oneshot { + if animation_ix >= self.animations.len() - 1 { + done = true; + } else { + state.start = Instant::now(); + state.animation_ix += 1; + } + delta = 1.0; + } else { + delta %= 1.0; + } + } + (animation_ix, delta, done) + }; + let delta = (self.animations[animation_ix].easing)(delta); + + debug_assert!(delta.is_finite(), "animated value should be finite"); + + let element = self.element.take().expect("should only be called once"); + let mut element = (self.animator)(element, animation_ix, delta).into_any_element(); + + if !done { + match self.animations[animation_ix].max_fps { + Some(max_fps) if max_fps.is_finite() && max_fps > 0.0 => { + if !state.delayed_frame_pending.get() { + state.delayed_frame_pending.set(true); + let delayed_frame_pending = state.delayed_frame_pending.clone(); + let view = window.current_view(); + let interval = Duration::from_secs_f32(1.0 / max_fps); + window + .spawn(cx, async move |cx| { + cx.background_executor().timer(interval).await; + delayed_frame_pending.set(false); + cx.update(move |_, cx| cx.notify(view)).ok(); + }) + .detach(); + } + } + _ => window.request_animation_frame(), + } + } + + ((element.request_layout(window, cx), element), state) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + element.prepaint(window, cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: crate::Bounds, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + element.paint(window, cx); + } +} + +mod easing { + use std::f32::consts::PI; + + /// The linear easing function, or delta itself + pub fn linear(delta: f32) -> f32 { + delta + } + + /// The quadratic easing function, delta * delta + pub fn quadratic(delta: f32) -> f32 { + delta * delta + } + + /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle + pub fn ease_in_out(delta: f32) -> f32 { + if delta < 0.5 { + 2.0 * delta * delta + } else { + let x = -2.0 * delta + 2.0; + 1.0 - x * x / 2.0 + } + } + + /// The Quint ease-out function, which starts quickly and decelerates to a stop + pub fn ease_out_quint() -> impl Fn(f32) -> f32 { + move |delta| 1.0 - (1.0 - delta).powi(5) + } + + /// Apply the given easing function, first in the forward direction and then in the reverse direction + pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { + move |delta| { + if delta < 0.5 { + easing(delta * 2.0) + } else { + easing((1.0 - delta) * 2.0) + } + } + } + + /// A custom easing function for pulsating alpha that slows down as it approaches 0.1 + pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 { + let range = max - min; + + move |delta| { + // Use a combination of sine and cubic functions for a more natural breathing rhythm + let t = (delta * 2.0 * PI).sin(); + let breath = (t * t * t + t) / 2.0; + + // Map the breath to our desired alpha range + let normalized_alpha = (breath + 1.0) / 2.0; + + min + (normalized_alpha * range) + } + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc, time::Duration}; + + use crate::{ + Animation, Context, InteractiveElement, Pixels, Render, SpringAnimation, SpringConfig, + TestAppContext, WindowHandle, div, prelude::*, px, size, + }; + + use super::*; + + struct AnimationTestView { + rendered_deltas: Rc>>, + max_fps: Option, + } + + struct SyncedAnimationTestView { + show_second: bool, + first_deltas: Rc>>, + second_deltas: Rc>>, + } + + struct SpringAnimationTestView { + target: Pixels, + initial: Option, + playback: SpringPlayback, + rendered_values: Rc>>, + } + + impl Render for SpringAnimationTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let rendered_values = self.rendered_values.clone(); + let mut animation = SpringAnimation::new(SpringConfig::new(100.0, 2.0, 1.0)) + .to(self.target) + .with_epsilon(0.01) + .playback(self.playback); + if let Some(initial) = self.initial { + animation = animation.from(initial); + } + div().with_spring("spring-animation", animation, move |this, value| { + rendered_values.borrow_mut().push(value); + this.left(value) + }) + } + } + + impl Render for SyncedAnimationTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let record_deltas = |deltas: Rc>>| { + move |this, delta| { + deltas.borrow_mut().push(delta); + this + } + }; + div() + .size_full() + .child(div().with_animation( + "first-synced-animation", + Animation::new(Duration::from_secs(1)).repeat_synced(), + record_deltas(self.first_deltas.clone()), + )) + .when(self.show_second, |this| { + this.child(div().with_animation( + "second-synced-animation", + Animation::new(Duration::from_secs(1)).repeat_synced(), + record_deltas(self.second_deltas.clone()), + )) + }) + } + } + + impl Render for AnimationTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let rendered_deltas = self.rendered_deltas.clone(); + // The throttled variant syncs to the shared clock so the deltas + // follow the test scheduler's clock rather than wall time. + let mut animation = Animation::new(Duration::from_secs(1)); + if let Some(max_fps) = self.max_fps { + animation = animation.repeat_synced().with_max_fps(max_fps); + } else { + animation = animation.repeat(); + } + div().size_full().child(div().with_animation( + "repeating-animation", + animation, + move |this, delta| { + rendered_deltas.borrow_mut().push(delta); + this + }, + )) + } + } + + fn open_test_window( + cx: &mut TestAppContext, + ) -> (Rc>>, WindowHandle) { + open_test_window_with_max_fps(cx, None) + } + + fn open_test_window_with_max_fps( + cx: &mut TestAppContext, + max_fps: Option, + ) -> (Rc>>, WindowHandle) { + let rendered_deltas = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.), px(100.)), { + let rendered_deltas = rendered_deltas.clone(); + move |_, _| AnimationTestView { + rendered_deltas, + max_fps, + } + }); + cx.run_until_parked(); + (rendered_deltas, window) + } + + fn simulate_next_frame(window: &WindowHandle, cx: &mut TestAppContext) -> usize { + let callback_count = window + .update(cx, |_, window, cx| window.simulate_next_frame(cx)) + .unwrap(); + cx.run_until_parked(); + callback_count + } + // Before parent-animation-element, using .with_animation + // would not allow chaining .parent after. This is just a + // build check that we can call div().id().with_animation().child() + #[test] + fn test_animation_parent() { + div() + .id("id") + // + .with_animation( + "animation", + Animation::new(Duration::from_secs(1)), + |el, _t| { + // + el + }, + ) + .child( + // + div(), + ); + } + + #[test] + fn test_spring_animation_parent() { + div() + .id("id") + .with_spring( + "spring-animation", + SpringAnimation::new(SpringConfig::new(100.0, 10.0, 1.0)) + .to(px(10.0)) + .from(px(0.0)), + |element, value| element.left(value), + ) + .child(div()); + } + + #[gpui::test] + fn test_spring_animation_preserves_velocity_when_retargeted(cx: &mut TestAppContext) { + let rendered_values = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.0), px(100.0)), { + let rendered_values = rendered_values.clone(); + move |_, _| SpringAnimationTestView { + target: px(0.0), + initial: None, + playback: SpringPlayback::Running, + rendered_values, + } + }); + cx.run_until_parked(); + assert_eq!(*rendered_values.borrow(), vec![px(0.0)]); + + window + .update(cx, |view, _, cx| { + view.target = px(100.0); + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + + cx.executor().advance_clock(Duration::from_millis(50)); + assert!(simulate_next_frame(&window, cx) > 0); + let value_before_retargeting = *rendered_values.borrow().last().unwrap(); + assert!(value_before_retargeting > px(0.0)); + assert!(value_before_retargeting < px(100.0)); + + window + .update(cx, |view, _, cx| { + view.target = px(0.0); + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + + cx.executor().advance_clock(Duration::from_millis(5)); + assert!(simulate_next_frame(&window, cx) > 0); + let value_after_retargeting = *rendered_values.borrow().last().unwrap(); + assert!(value_after_retargeting > value_before_retargeting); + } + + #[gpui::test] + fn test_paused_spring_resumes_with_its_velocity(cx: &mut TestAppContext) { + let rendered_values = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.0), px(100.0)), { + let rendered_values = rendered_values.clone(); + move |_, _| SpringAnimationTestView { + target: px(0.0), + initial: None, + playback: SpringPlayback::Running, + rendered_values, + } + }); + cx.run_until_parked(); + + window + .update(cx, |view, _, cx| { + view.target = px(100.0); + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(50)); + assert!(simulate_next_frame(&window, cx) > 0); + + window + .update(cx, |view, _, cx| { + view.target = px(0.0); + view.playback = SpringPlayback::Paused; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + let paused_value = *rendered_values.borrow().last().unwrap(); + + cx.executor().advance_clock(Duration::from_millis(500)); + assert!(simulate_next_frame(&window, cx) > 0); + assert_eq!(*rendered_values.borrow().last().unwrap(), paused_value); + assert_eq!(simulate_next_frame(&window, cx), 0); + + window + .update(cx, |view, _, cx| { + view.playback = SpringPlayback::Running; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(5)); + assert!(simulate_next_frame(&window, cx) > 0); + assert!(*rendered_values.borrow().last().unwrap() > paused_value); + } + + #[gpui::test] + fn test_stopped_spring_resumes_without_velocity(cx: &mut TestAppContext) { + let rendered_values = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.0), px(100.0)), { + let rendered_values = rendered_values.clone(); + move |_, _| SpringAnimationTestView { + target: px(0.0), + initial: None, + playback: SpringPlayback::Running, + rendered_values, + } + }); + cx.run_until_parked(); + + window + .update(cx, |view, _, cx| { + view.target = px(1_000_000.0); + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(50)); + assert!(simulate_next_frame(&window, cx) > 0); + + window + .update(cx, |view, _, cx| { + view.target = px(0.0); + view.playback = SpringPlayback::Stopped; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + let stopped_value = *rendered_values.borrow().last().unwrap(); + + cx.executor().advance_clock(Duration::from_millis(500)); + assert!(simulate_next_frame(&window, cx) > 0); + assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value); + assert_eq!(simulate_next_frame(&window, cx), 0); + + window + .update(cx, |view, _, cx| { + view.target = stopped_value; + view.playback = SpringPlayback::Running; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + assert_eq!(*rendered_values.borrow().last().unwrap(), stopped_value); + assert_eq!(simulate_next_frame(&window, cx), 0); + } + + #[gpui::test] + fn test_cancelled_and_completed_springs_resolve_their_endpoints(cx: &mut TestAppContext) { + let rendered_values = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.0), px(100.0)), { + let rendered_values = rendered_values.clone(); + move |_, _| SpringAnimationTestView { + target: px(100.0), + initial: Some(px(20.0)), + playback: SpringPlayback::Running, + rendered_values, + } + }); + cx.run_until_parked(); + assert_eq!(*rendered_values.borrow(), vec![px(20.0)]); + + cx.executor().advance_clock(Duration::from_millis(50)); + assert!(simulate_next_frame(&window, cx) > 0); + assert!(*rendered_values.borrow().last().unwrap() > px(20.0)); + + window + .update(cx, |view, _, cx| { + view.playback = SpringPlayback::Cancelled; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + assert_eq!(*rendered_values.borrow().last().unwrap(), px(20.0)); + assert!(simulate_next_frame(&window, cx) > 0); + assert_eq!(simulate_next_frame(&window, cx), 0); + + window + .update(cx, |view, _, cx| { + view.playback = SpringPlayback::Completed; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + assert_eq!(*rendered_values.borrow().last().unwrap(), px(100.0)); + assert_eq!(simulate_next_frame(&window, cx), 0); + } + + #[gpui::test] + fn test_spring_animation_respects_reduced_motion(cx: &mut TestAppContext) { + cx.update(|cx| cx.set_reduce_motion(true)); + let rendered_values = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.0), px(100.0)), { + let rendered_values = rendered_values.clone(); + move |_, _| SpringAnimationTestView { + target: px(100.0), + initial: None, + playback: SpringPlayback::Running, + rendered_values, + } + }); + cx.run_until_parked(); + + assert_eq!(*rendered_values.borrow(), vec![px(100.0)]); + assert_eq!(simulate_next_frame(&window, cx), 0); + } + + #[gpui::test] + fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) { + let (rendered_deltas, window) = open_test_window(cx); + + assert_eq!(rendered_deltas.borrow().len(), 1); + + for expected_frames in 2..=3 { + assert_eq!(simulate_next_frame(&window, cx), 1); + assert_eq!(rendered_deltas.borrow().len(), expected_frames); + } + } + + #[gpui::test] + fn test_max_fps_schedules_timer_driven_frames(cx: &mut TestAppContext) { + let (rendered_deltas, window) = open_test_window_with_max_fps(cx, Some(10.0)); + + // The test scheduler's clock jitters forward slightly on each poll, + // so compare against expectations loosely. + let assert_deltas_approx_eq = |expected: &[f32]| { + let actual = rendered_deltas.borrow(); + assert_eq!(actual.len(), expected.len(), "deltas: {actual:?}"); + for (actual, expected) in actual.iter().zip(expected) { + assert!( + (actual - expected).abs() < 1e-2, + "expected {expected}, got {actual}" + ); + } + }; + + assert_deltas_approx_eq(&[0.0]); + + // No per-frame callback is scheduled; re-renders are timer-driven. + assert_eq!(simulate_next_frame(&window, cx), 0); + assert_deltas_approx_eq(&[0.0]); + + cx.executor().advance_clock(Duration::from_millis(105)); + cx.run_until_parked(); + assert_deltas_approx_eq(&[0.0, 0.105]); + + cx.executor().advance_clock(Duration::from_millis(105)); + cx.run_until_parked(); + assert_deltas_approx_eq(&[0.0, 0.105, 0.21]); + } + + #[gpui::test] + fn test_synced_animations_share_phase_across_elements(cx: &mut TestAppContext) { + let first_deltas = Rc::new(RefCell::new(Vec::new())); + let second_deltas = Rc::new(RefCell::new(Vec::new())); + let window = cx.open_window(size(px(100.), px(100.)), { + let first_deltas = first_deltas.clone(); + let second_deltas = second_deltas.clone(); + move |_, _| SyncedAnimationTestView { + show_second: false, + first_deltas, + second_deltas, + } + }); + cx.run_until_parked(); + + assert_eq!(*first_deltas.borrow(), vec![0.0]); + + cx.executor().advance_clock(Duration::from_millis(250)); + simulate_next_frame(&window, cx); + assert_eq!(*first_deltas.borrow(), vec![0.0, 0.25]); + + // The second element mounts a quarter through the cycle, yet renders + // the shared phase rather than starting at zero. + window + .update(cx, |view, _, cx| { + view.show_second = true; + cx.notify(); + }) + .unwrap(); + cx.run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(250)); + simulate_next_frame(&window, cx); + + assert_eq!(*second_deltas.borrow().last().unwrap(), 0.5); + assert_eq!( + *first_deltas.borrow().last().unwrap(), + *second_deltas.borrow().last().unwrap() + ); + assert!(second_deltas.borrow().iter().all(|delta| *delta > 0.0)); + + // The phase wraps around each full cycle. + cx.executor().advance_clock(Duration::from_millis(2250)); + simulate_next_frame(&window, cx); + assert_eq!(*first_deltas.borrow().last().unwrap(), 0.75); + + // Sub-second precision survives months of uptime: converting the raw + // elapsed time to f32 would round 0.25 away entirely. + cx.executor() + .advance_clock(Duration::from_secs(300 * 24 * 60 * 60) + Duration::from_millis(500)); + simulate_next_frame(&window, cx); + assert_eq!(*first_deltas.borrow().last().unwrap(), 0.25); + } + + #[gpui::test] + fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) { + cx.update(|cx| cx.set_reduce_motion(true)); + let (rendered_deltas, window) = open_test_window(cx); + + assert_eq!(*rendered_deltas.borrow(), vec![0.0]); + + assert_eq!(simulate_next_frame(&window, cx), 0); + assert_eq!(*rendered_deltas.borrow(), vec![0.0]); + } +} diff --git a/vendor/gpui-pre/src/elements/canvas.rs b/vendor/gpui-pre/src/elements/canvas.rs new file mode 100644 index 0000000..d57d2f6 --- /dev/null +++ b/vendor/gpui-pre/src/elements/canvas.rs @@ -0,0 +1,95 @@ +use refineable::Refineable as _; + +use crate::{ + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Pixels, + Style, StyleRefinement, Styled, Window, +}; + +/// Construct a canvas element with the given paint callback. +/// Useful for adding short term custom drawing to a view. +pub fn canvas( + prepaint: impl 'static + FnOnce(Bounds, &mut Window, &mut App) -> T, + paint: impl 'static + FnOnce(Bounds, T, &mut Window, &mut App), +) -> Canvas { + Canvas { + prepaint: Some(Box::new(prepaint)), + paint: Some(Box::new(paint)), + style: StyleRefinement::default(), + } +} + +/// A canvas element, meant for accessing the low level paint API without defining a whole +/// custom element +pub struct Canvas { + prepaint: Option, &mut Window, &mut App) -> T>>, + paint: Option, T, &mut Window, &mut App)>>, + style: StyleRefinement, +} + +impl IntoElement for Canvas { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Canvas { + type RequestLayoutState = Style; + type PrepaintState = Option; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style.clone(), [], cx); + (layout_id, style) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Style, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(self.prepaint.take().unwrap()(bounds, window, cx)) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + style: &mut Style, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let prepaint = prepaint.take().unwrap(); + style.paint(bounds, window, cx, |window, cx| { + (self.paint.take().unwrap())(bounds, prepaint, window, cx) + }); + } +} + +impl Styled for Canvas { + fn style(&mut self) -> &mut crate::StyleRefinement { + &mut self.style + } +} diff --git a/vendor/gpui-pre/src/elements/container_query.rs b/vendor/gpui-pre/src/elements/container_query.rs new file mode 100644 index 0000000..0363ce6 --- /dev/null +++ b/vendor/gpui-pre/src/elements/container_query.rs @@ -0,0 +1,126 @@ +//! A container query element, in the spirit of CSS container queries. +//! The element's own size is determined solely by its style and the space +//! offered by its parent. + +use refineable::Refineable as _; + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Pixels, Size, Style, StyleRefinement, Styled, + Window, relative, +}; + +/// Construct a container query element with the given render callback. +/// The callback receives the size the element was assigned during layout and +/// returns the contents to display within it. +/// +/// By default the element fills its parent (equivalent to `.size_full()`); +/// use the [`Styled`] methods to size it differently. Because the contents +/// don't exist until after layout, they cannot influence the element's size. +/// +/// # Example +/// +/// ``` +/// # use gpui::{container_query, div, px, IntoElement, ParentElement}; +/// container_query(|size, _window, _cx| { +/// if size.width < px(240.) { +/// div().child("Narrow layout") +/// } else { +/// div().child("Wide layout") +/// } +/// }); +/// ``` +pub fn container_query( + render: impl 'static + FnOnce(Size, &mut Window, &mut App) -> E, +) -> ContainerQuery +where + E: IntoElement, +{ + let mut base_style = StyleRefinement::default(); + base_style.size.width = Some(relative(1.).into()); + base_style.size.height = Some(relative(1.).into()); + + ContainerQuery { + render: Some(Box::new(|size, window, cx| { + render(size, window, cx).into_any_element() + })), + style: base_style, + } +} + +/// A container query element, created with [`container_query`]. +pub struct ContainerQuery { + render: Option, &mut Window, &mut App) -> AnyElement>>, + style: StyleRefinement, +} + +impl Element for ContainerQuery { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let render = self.render.take()?; + let mut child = render(bounds.size, window, cx); + child.layout_as_root(bounds.size.map(AvailableSpace::Definite), window, cx); + child.prepaint_at(bounds.origin, window, cx); + Some(child) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + if let Some(child) = prepaint { + child.paint(window, cx); + } + } +} + +impl IntoElement for ContainerQuery { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for ContainerQuery { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/vendor/gpui-pre/src/elements/deferred.rs b/vendor/gpui-pre/src/elements/deferred.rs new file mode 100644 index 0000000..ddf324a --- /dev/null +++ b/vendor/gpui-pre/src/elements/deferred.rs @@ -0,0 +1,206 @@ +use crate::{ + AnyElement, App, Bounds, Element, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + Pixels, Window, +}; + +/// Builds a `Deferred` element, which delays the layout and paint of its child. +pub fn deferred(child: impl IntoElement) -> Deferred { + Deferred { + child: Some(child.into_any_element()), + priority: 0, + } +} + +/// An element which delays the painting of its child until after all of +/// its ancestors, while keeping its layout as part of the current element tree. +pub struct Deferred { + child: Option, + priority: usize, +} + +impl Deferred { + /// Sets the `priority` value of the `deferred` element, which + /// determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + pub fn with_priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} + +impl Element for Deferred { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, ()) { + let layout_id = self.child.as_mut().unwrap().request_layout(window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + _cx: &mut App, + ) { + let child = self.child.take().unwrap(); + let element_offset = window.element_offset(); + window.defer_draw(child, element_offset, self.priority, None) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _window: &mut Window, + _cx: &mut App, + ) { + } +} + +impl IntoElement for Deferred { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Deferred { + /// Sets a priority for the element. A higher priority conceptually means painting the element + /// on top of deferred draws with a lower priority (i.e. closer to the viewer). + pub fn priority(mut self, priority: usize) -> Self { + self.priority = priority; + self + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Context, Entity, StyleRefinement, TestAppContext, Window, anchored, deferred, div, point, + prelude::*, px, size, + }; + + /// A stand-in for a dock panel hosting a popover (deferred draw) whose + /// content opens another popover (a deferred draw created while + /// prepainting the first one's content). + struct PanelView; + + impl Render for PanelView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().key_context("Panel").size_full().child( + deferred( + anchored().position(point(px(10.), px(10.))).child( + div().key_context("Popover").w(px(200.)).h(px(200.)).child( + deferred( + anchored().position(point(px(30.), px(30.))).child( + div() + .key_context("NestedMenu") + .debug_selector(|| "NESTED_MENU".into()) + .w(px(50.)) + .h(px(50.)), + ), + ) + .with_priority(2), + ), + ), + ) + .with_priority(1), + ) + } + } + + struct RootView { + panel: Entity, + } + + impl Render for RootView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().key_context("Root").size_full().child( + self.panel + .clone() + .cached(StyleRefinement::default().size_full()), + ) + } + } + + /// Regression test for a crash with nested deferred draws (e.g. a popover + /// menu inside a popover hosted by a cached dock panel). Prepaint indices + /// recorded during the deferred draw rounds must index the same + /// `deferred_draws` vector that `reuse_prepaint` slices on the next frame; + /// previously they were measured against a transient per-round vector, so + /// reusing the panel's subtree grafted the wrong deferred draws and + /// panicked in the dispatch tree. + #[gpui::test] + fn test_nested_deferred_draws_with_reused_views(cx: &mut TestAppContext) { + let window = cx.open_window(size(px(800.), px(600.)), |_, cx| { + let panel = cx.new(|_| PanelView); + RootView { panel } + }); + cx.run_until_parked(); + + let menu_bounds = window + .update(cx, |_, window, _| { + window + .rendered_frame + .debug_bounds + .get("NESTED_MENU") + .copied() + }) + .unwrap() + .expect("NESTED_MENU debug bounds not found"); + assert_eq!(menu_bounds.size, size(px(50.), px(50.))); + + // Re-render only the root view; the panel is cached, so its subtree - + // including both deferred draw records - is reused from the previous + // frame. + window.update(cx, |_, _, cx| cx.notify()).unwrap(); + cx.run_until_parked(); + + // Reuse the subtree a second time, exercising ranges that were + // themselves recorded during a reused frame. + window.update(cx, |_, _, cx| cx.notify()).unwrap(); + cx.run_until_parked(); + + // Re-render the panel itself again to prove the popovers still draw. + window + .update(cx, |root, _, cx| { + root.panel.update(cx, |_, cx| cx.notify()); + }) + .unwrap(); + cx.run_until_parked(); + + window + .update(cx, |_, window, _| { + assert_eq!(window.rendered_frame.deferred_draws.len(), 2); + assert!( + window + .rendered_frame + .debug_bounds + .contains_key("NESTED_MENU") + ); + }) + .unwrap(); + } +} diff --git a/vendor/gpui-pre/src/elements/div.rs b/vendor/gpui-pre/src/elements/div.rs new file mode 100644 index 0000000..fc292f2 --- /dev/null +++ b/vendor/gpui-pre/src/elements/div.rs @@ -0,0 +1,5640 @@ +//! Div is the central, reusable element that most GPUI trees will be built from. +//! It functions as a container for other elements, and provides a number of +//! useful features for laying out and styling its children as well as binding +//! mouse events and action handlers. It is meant to be similar to the HTML `
` +//! element, but for GPUI. +//! +//! # Build your own div +//! +//! GPUI does not directly provide APIs for stateful, multi step events like `click` +//! and `drag`. We want GPUI users to be able to build their own abstractions for +//! their own needs. However, as a UI framework, we're also obliged to provide some +//! building blocks to make the process of building your own elements easier. +//! For this we have the [`Interactivity`] and the [`StyleRefinement`] structs, as well +//! as several associated traits. Together, these provide the full suite of Dom-like events +//! and Tailwind-like styling that you can use to build your own custom elements. Div is +//! constructed by combining these two systems into an all-in-one element. + +use crate::{ + Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, + Display, Element, ElementId, Entity, EntityId, ExternalDragPayload, ExternalDragPayloadSource, + FileDropEvent, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, HitboxId, + InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, KeyboardButton, + KeyboardClickEvent, LayoutId, LongPressEvent, ModifiersChangedEvent, MouseButton, + MouseClickEvent, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, + MouseUpEvent, OngoingScroll, Overflow, ParentElement, PinchEvent, Pixels, Point, Render, + ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, + TouchPhase, Visibility, Window, WindowControlArea, point, px, size, +}; +use collections::HashMap; +use gpui_util::ResultExt; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + cell::{Cell, RefCell}, + cmp::Ordering, + fmt::Debug, + marker::PhantomData, + mem, + rc::Rc, + sync::Arc, + time::Duration, +}; + +use super::ImageCacheProvider; + +#[cfg(feature = "stacker")] +type StackSafe = stacksafe::StackSafe; +#[cfg(not(feature = "stacker"))] +type StackSafe = T; + +const DRAG_THRESHOLD: f64 = 2.; +const DEFAULT_TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500); +const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500); + +/// The styling information for a given group. +pub struct GroupStyle { + /// The identifier for this group. + pub group: SharedString, + + /// The specific style refinement that this group would apply + /// to its children. + pub style: Box, +} + +/// An event for when a drag is moving over this element, with the given state type. +pub struct DragMoveEvent { + /// The mouse move event that triggered this drag move event. + pub event: MouseMoveEvent, + + /// The bounds of this element. + pub bounds: Bounds, + drag: PhantomData, + dragged_item: Arc, +} + +impl DragMoveEvent { + /// Returns the drag state for this event. + pub fn drag<'b>(&self, cx: &'b App) -> &'b T { + cx.active_drag + .as_ref() + .and_then(|drag| drag.value.downcast_ref::()) + .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") + } + + /// An item that is about to be dropped. + pub fn dragged_item(&self) -> &dyn Any { + self.dragged_item.as_ref() + } +} + +impl Interactivity { + /// Create an `Interactivity`, capturing the caller location in debug mode. + #[cfg(any(feature = "inspector", debug_assertions))] + #[track_caller] + pub fn new() -> Interactivity { + Interactivity { + source_location: Some(core::panic::Location::caller()), + ..Default::default() + } + } + + /// Create an `Interactivity`, capturing the caller location in debug mode. + #[cfg(not(any(feature = "inspector", debug_assertions)))] + pub fn new() -> Interactivity { + Interactivity::default() + } + + /// Gets the source location of construction. Returns `None` when not in debug mode. + pub fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + #[cfg(any(feature = "inspector", debug_assertions))] + { + self.source_location + } + + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + None + } + } + + /// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase. + /// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. + pub fn on_mouse_down( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && hitbox.is_hovered(window) + { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase. + /// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event for any button, during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_any_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse pressure event, during the bubble phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_pressure( + &mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_pressure_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse pressure event, during the capture phase + /// the imperative API equivalent to [`InteractiveElement::on_mouse_pressure`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_mouse_pressure( + &mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_pressure_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_up( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && hitbox.is_hovered(window) + { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase. + /// The imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event for any button, during the bubble phase. + /// The imperative API equivalent to [`Interactivity::on_any_mouse_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_down_out( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture + && !window.has_active_prompt() + && !hitbox.contains(&window.mouse_position()) + { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_up_out( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !hitbox.is_hovered(window) + { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to the mouse move event, during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_move`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_move( + &mut self, + listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to the mouse exit event, during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_mouse_exit`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_mouse_exit( + &mut self, + listener: impl Fn(&MouseExitEvent, &mut Window, &mut App) + 'static, + ) { + self.mouse_exit_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to [`FileDropEvent::Exited`] when a platform file drag + /// leaves this element's window while the element is hovered. + /// + /// This is a window-local exit event, not notification that the platform drag session ended. + /// The imperative API equivalent to [`InteractiveElement::on_file_drop_exit`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_file_drop_exit( + &mut self, + listener: impl Fn(&FileDropEvent, &mut Window, &mut App) + 'static, + ) { + self.file_drop_exit_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble + && matches!(event, FileDropEvent::Exited) + && hitbox.id.is_hovered_ignoring_last_input(window) + { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The imperative API equivalent to [`InteractiveElement::on_drag_move`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drag_move( + &mut self, + listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, + ) where + T: 'static, + { + self.mouse_move_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Capture + && let Some(drag) = &cx.active_drag + && drag.value.as_ref().type_id() == TypeId::of::() + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + window, + cx, + ); + } + })); + } + + /// Bind the given callback to scroll wheel events during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_scroll_wheel( + &mut self, + listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, + ) { + self.scroll_wheel_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to pinch gesture events during the bubble phase. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) { + self.pinch_listeners + .push(Box::new(move |event, phase, hitbox, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + (listener)(event, window, cx); + } + })); + } + + /// Bind the given callback to pinch gesture events during the capture phase. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_pinch( + &mut self, + listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, + ) { + self.pinch_listeners + .push(Box::new(move |event, phase, _hitbox, window, cx| { + if phase == DispatchPhase::Capture { + (listener)(event, window, cx); + } else { + cx.propagate(); + } + })); + } + + /// Bind the given callback to an action dispatch during the capture phase. + /// The imperative API equivalent to [`InteractiveElement::capture_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_action( + &mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + (listener)(action, window, cx) + } else { + cx.propagate(); + } + }), + )); + } + + /// Bind the given callback to an action dispatch during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + #[track_caller] + #[inline(always)] + pub fn on_action(&mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + (listener)(action, window, cx) + } + }), + )); + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The imperative API equivalent to [`InteractiveElement::on_boxed_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_boxed_action( + &mut self, + action: &dyn Action, + listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, + ) { + let action = action.boxed_clone(); + self.action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, window, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&*action, window, cx) + } + }), + )); + } + + /// Bind the given callback to key down events during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_key_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Bubble { + (listener)(event, window, cx) + } + })); + } + + /// Bind the given callback to key down events during the capture phase. + /// The imperative API equivalent to [`InteractiveElement::capture_key_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Capture { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to key up events during the bubble phase. + /// The imperative API equivalent to [`InteractiveElement::on_key_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Bubble { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to key up events during the capture phase. + /// The imperative API equivalent to [`InteractiveElement::on_key_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn capture_key_up( + &mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) { + self.key_up_listeners + .push(Box::new(move |event, phase, window, cx| { + if phase == DispatchPhase::Capture { + listener(event, window, cx) + } + })); + } + + /// Bind the given callback to modifiers changing events. + /// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) { + self.modifiers_changed_listeners.push(Box::new(listener)); + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element. + /// The imperative API equivalent to [`InteractiveElement::on_drop`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drop(&mut self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) { + self.drop_listeners.push(( + TypeId::of::(), + Box::new(move |dragged_value, window, cx| { + listener(dragged_value.downcast_ref().unwrap(), window, cx); + }), + )); + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element. + /// The imperative API equivalent to [`InteractiveElement::can_drop`]. + pub fn can_drop( + &mut self, + predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, + ) { + self.can_drop_predicate = Some(Box::new(predicate)); + } + + /// Bind the given callback to click events of this element. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_click`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + #[inline(always)] + pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) + where + Self: Sized, + { + self.click_listeners.push(Rc::new(listener)); + } + + /// Bind the given callback to non-primary click events of this element. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_aux_click`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_aux_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) + where + Self: Sized, + { + self.aux_click_listeners.push(Rc::new(listener)); + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`Self::on_drag_move`] API. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_drag( + &mut self, + value: T, + constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, + ) where + Self: Sized, + T: 'static, + W: 'static + Render, + { + debug_assert!( + self.drag_listener.is_none(), + "calling on_drag more than once on the same element is not supported" + ); + self.drag_listener = Some(DragListener { + value: Arc::new(value), + render: Box::new(move |value, offset, window, cx| { + constructor(value.downcast_ref().unwrap(), offset, window, cx).into() + }), + external_payload: None, + }); + } + + /// Registers a callback resolving a payload to offer the platform if a drag started by this + /// element leaves the window. It is invoked at most once per drag gesture, when the pointer + /// exits the viewport. Must be called after [`Self::on_drag`], with the same dragged value + /// type `T`. + pub fn external_drag_payload( + &mut self, + resolver: impl Fn(&T, &mut Window, &mut App) -> Option + 'static, + ) where + Self: Sized, + T: 'static, + { + let Some(drag_listener) = self.drag_listener.as_mut() else { + debug_assert!(false, "external_drag_payload must be called after on_drag"); + return; + }; + debug_assert!( + drag_listener.value.as_ref().type_id() == TypeId::of::(), + "external_drag_payload must use the same dragged value type as on_drag" + ); + debug_assert!( + drag_listener.external_payload.is_none(), + "calling external_drag_payload more than once on the same element is not supported" + ); + drag_listener.external_payload = Some(Box::new(move |value, window, cx| { + resolver(value.downcast_ref::()?, window, cx) + })); + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// Transitions caused by layout changes under a stationary mouse also invoke the callback. + /// + /// By default, keyboard input suppresses hover until the next mouse move, mouse down, or touch. Set + /// [`HoverListenerMode::InputModalityIndependent`] with [`Self::hover_listener_mode`] to + /// continue hit-testing hover after keyboard input. + /// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) + where + Self: Sized, + { + debug_assert!( + self.hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.hover_listener = Some(Box::new(listener)); + } + + /// Sets how [`Self::on_hover`] responds to key presses while the mouse is stationary. + /// This affects only the hover listener, not hover styles or tooltips. The imperative API + /// equivalent to [`StatefulInteractiveElement::hover_listener_mode`]. + pub fn hover_listener_mode(&mut self, mode: HoverListenerMode) + where + Self: Sized, + { + self.hover_listener_mode = mode; + } + + /// Constructs a tooltip when the element is hovered or long-pressed. + /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`]. + pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) + where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: false, + }); + } + + /// Constructs a tooltip when the element is hovered or long-pressed. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`]. + pub fn hoverable_tooltip( + &mut self, + build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(TooltipBuilder { + build: Rc::new(build_tooltip), + hoverable: true, + }); + } + + /// Sets the delay before this element's tooltip is shown on hover. + /// + /// Touch long presses show the tooltip immediately once the gesture is recognized. + /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip_show_delay`]. + pub fn tooltip_show_delay(&mut self, delay: Duration) { + self.tooltip_show_delay = Some(delay); + } + + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. + /// + /// The imperative API equivalent to [`InteractiveElement::occlude`] + pub fn occlude_mouse(&mut self) { + self.hitbox_behavior = HitboxBehavior::BlockMouse; + } + + /// Set the bounds of this element as a window control area for the platform window. + /// The imperative API equivalent to [`InteractiveElement::window_control_area`] + pub fn window_control_area(&mut self, area: WindowControlArea) { + self.window_control = Some(area); + } + + /// Block non-scroll mouse interactions with elements behind this element's hitbox. + /// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]. + /// + /// See [`Hitbox::is_hovered`] for details. + pub fn block_mouse_except_scroll(&mut self) { + self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll; + } + + fn has_pinch_listeners(&self) -> bool { + !self.pinch_listeners.is_empty() + } +} + +/// A trait for elements that want to use the standard GPUI event handlers that don't +/// require any state. +pub trait InteractiveElement: Sized { + /// Retrieve the interactivity state associated with this element + fn interactivity(&mut self) -> &mut Interactivity; + + /// Assign this element to a group of elements that can be styled together + fn group(mut self, group: impl Into) -> Self { + self.interactivity().group = Some(group.into()); + self + } + + /// Assign this element an ID, so that it can be used with interactivity + fn id(mut self, id: impl Into) -> Stateful { + self.interactivity().element_id = Some(id.into()); + + Stateful { element: self } + } + + /// Track the focus state of the given focus handle on this element. + /// If the focus handle is focused by the application, this element will + /// apply its focused styles. + fn track_focus(mut self, focus_handle: &FocusHandle) -> Self { + self.interactivity().focusable = true; + self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); + self + } + + /// Set whether this element is a tab stop. + /// + /// When false, the element remains in tab-index order but cannot be reached via keyboard navigation. + /// Useful for container elements: focus the container, then call `window.focus_next(cx)` to focus + /// the first tab stop inside it while having the container element itself be unreachable via the keyboard. + /// Should only be used with `tab_index`. + fn tab_stop(mut self, tab_stop: bool) -> Self { + self.interactivity().tab_stop = tab_stop; + self + } + + /// Set index of the tab stop order, and set this node as a tab stop. + /// This will default the element to being a tab stop. See [`Self::tab_stop`] for more information. + /// This should only be used in conjunction with `tab_group` + /// in order to not interfere with the tab index of other elements. + fn tab_index(mut self, index: isize) -> Self { + self.interactivity().focusable = true; + self.interactivity().tab_index = Some(index); + self.interactivity().tab_stop = true; + self + } + + /// Designate this div as a "tab group". Tab groups have their own location in the tab-index order, + /// but for children of the tab group, the tab index is reset to 0. This can be useful for swapping + /// the order of tab stops within the group, without having to renumber all the tab stops in the whole + /// application. + fn tab_group(mut self) -> Self { + self.interactivity().tab_group = true; + if self.interactivity().tab_index.is_none() { + self.interactivity().tab_index = Some(0); + } + self + } + + /// Set the keymap context for this element. This will be used to determine + /// which action to dispatch from the keymap. + fn key_context(mut self, key_context: C) -> Self + where + C: TryInto, + E: std::fmt::Display, + { + if let Some(key_context) = key_context.try_into().log_err() { + self.interactivity().key_context = Some(key_context); + } + self + } + + /// Apply the given style to this element when the mouse hovers over it + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { + debug_assert!( + self.interactivity().hover_style.is_none(), + "hover style already set" + ); + self.interactivity().hover_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Apply the given style to this element when the mouse hovers over a group member + fn group_hover( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_hover_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to the mouse down event for the given mouse button. + /// The fluent API equivalent to [`Interactivity::on_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback. + #[inline(always)] + fn on_mouse_down( + mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_down(button, listener); + self + } + + /// Set a key that can be used to look up this element's bounds + /// in the [`crate::VisualTestContext::debug_bounds`] map. + /// + /// Selectors are only recorded in debug builds with the `test-support` + /// feature. Elsewhere this is a no-op, so `test-support` can stay enabled + /// in release builds without the per-frame cost. + #[cfg(any(test, all(debug_assertions, feature = "test-support")))] + fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { + self.interactivity().debug_selector = Some(f()); + self + } + + /// Set a key that can be used to look up this element's bounds + /// in the [`crate::VisualTestContext::debug_bounds`] map. + /// + /// Selectors are only recorded in debug builds with the `test-support` + /// feature. Elsewhere this is a no-op, so `test-support` can stay enabled + /// in release builds without the per-frame cost. + #[cfg(not(any(test, all(debug_assertions, feature = "test-support"))))] + #[inline] + fn debug_selector(self, _: impl FnOnce() -> String) -> Self { + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase. + /// The fluent API equivalent to [`Interactivity::capture_any_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse down event for any button, during the capture phase. + /// The fluent API equivalent to [`Interactivity::on_any_mouse_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_any_mouse_down( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_any_mouse_down(listener); + self + } + + /// Bind the given callback to the mouse up event for the given button, during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_mouse_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_up( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_up(button, listener); + self + } + + /// Bind the given callback to the mouse up event for any button, during the capture phase. + /// The fluent API equivalent to [`Interactivity::capture_any_mouse_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_any_mouse_up( + mut self, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_any_mouse_up(listener); + self + } + + /// Bind the given callback to the mouse pressure event, during the bubble phase + /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_pressure( + mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_pressure(listener); + self + } + + /// Bind the given callback to the mouse pressure event, during the capture phase + /// the fluent API equivalent to [`Interactivity::on_mouse_pressure`] + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_mouse_pressure( + mut self, + listener: impl Fn(&MousePressureEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_mouse_pressure(listener); + self + } + + /// Bind the given callback to the mouse down event, on any button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_down_out( + mut self, + listener: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_down_out(listener); + self + } + + /// Bind the given callback to the mouse up event, for the given button, during the capture phase, + /// when the mouse is outside of the bounds of this element. + /// The fluent API equivalent to [`Interactivity::on_mouse_up_out`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_up_out( + mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_up_out(button, listener); + self + } + + /// Bind the given callback to the mouse move event, during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_mouse_move`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_move( + mut self, + listener: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_move(listener); + self + } + + /// Bind the given callback to the mouse exit event, during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_mouse_exit`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_mouse_exit( + mut self, + listener: impl Fn(&MouseExitEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_mouse_exit(listener); + self + } + + /// Bind the given callback to [`FileDropEvent::Exited`] when a platform file drag + /// leaves this element's window while the element is hovered. + /// + /// This is a window-local exit event, not notification that the platform drag session ended. + /// The fluent API equivalent to [`Interactivity::on_file_drop_exit`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_file_drop_exit( + mut self, + listener: impl Fn(&FileDropEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_file_drop_exit(listener); + self + } + + /// Bind the given callback to the mouse drag event of the given type. Note that this + /// will be called for all move events, inside or outside of this element, as long as the + /// drag was started with this element under the mouse. Useful for implementing draggable + /// UIs that don't conform to a drag and drop style interaction, like resizing. + /// The fluent API equivalent to [`Interactivity::on_drag_move`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drag_move( + mut self, + listener: impl Fn(&DragMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_drag_move(listener); + self + } + + /// Bind the given callback to scroll wheel events during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_scroll_wheel( + mut self, + listener: impl Fn(&ScrollWheelEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_scroll_wheel(listener); + self + } + + /// Bind the given callback to pinch gesture events during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_pinch`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self { + self.interactivity().on_pinch(listener); + self + } + + /// Bind the given callback to pinch gesture events during the capture phase. + /// The fluent API equivalent to [`Interactivity::capture_pinch`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_pinch( + mut self, + listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_pinch(listener); + self + } + /// Capture the given action, before normal action dispatch can fire. + /// The fluent API equivalent to [`Interactivity::capture_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_action( + mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_action(listener); + self + } + + /// Bind the given callback to an action dispatch during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + #[track_caller] + #[inline(always)] + fn on_action( + mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_action(listener); + self + } + + /// Bind the given callback to an action dispatch, based on a dynamic action parameter + /// instead of a type parameter. Useful for component libraries that want to expose + /// action bindings to their users. + /// The fluent API equivalent to [`Interactivity::on_boxed_action`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_boxed_action( + mut self, + action: &dyn Action, + listener: impl Fn(&dyn Action, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_boxed_action(action, listener); + self + } + + /// Bind the given callback to key down events during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_key_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_key_down(listener); + self + } + + /// Bind the given callback to key down events during the capture phase. + /// The fluent API equivalent to [`Interactivity::capture_key_down`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_key_down( + mut self, + listener: impl Fn(&KeyDownEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_key_down(listener); + self + } + + /// Bind the given callback to key up events during the bubble phase. + /// The fluent API equivalent to [`Interactivity::on_key_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_key_up(listener); + self + } + + /// Bind the given callback to key up events during the capture phase. + /// The fluent API equivalent to [`Interactivity::capture_key_up`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn capture_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().capture_key_up(listener); + self + } + + /// Bind the given callback to modifiers changing events. + /// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + #[inline(always)] + fn on_modifiers_changed( + mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_modifiers_changed(listener); + self + } + + /// Apply the given style when the given data type is dragged over this element + fn drag_over( + mut self, + f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement, + ) -> Self { + self.interactivity().drag_over_styles.push(( + TypeId::of::(), + Box::new(move |currently_dragged: &dyn Any, window, cx| { + f( + StyleRefinement::default(), + currently_dragged.downcast_ref::().unwrap(), + window, + cx, + ) + }), + )); + self + } + + /// Apply the given style when the given data type is dragged over this element's group + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }, + )); + self + } + + /// Bind the given callback to drop events of the given type, whether or not the drag started on this element. + /// The fluent API equivalent to [`Interactivity::on_drop`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drop( + mut self, + listener: impl Fn(&T, &mut Window, &mut App) + 'static, + ) -> Self { + self.interactivity().on_drop(listener); + self + } + + /// Use the given predicate to determine whether or not a drop event should be dispatched to this element. + /// The fluent API equivalent to [`Interactivity::can_drop`]. + fn can_drop( + mut self, + predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static, + ) -> Self { + self.interactivity().can_drop(predicate); + self + } + + /// Block the mouse from all interactions with elements behind this element's hitbox. Typically + /// `block_mouse_except_scroll` should be preferred. + /// The fluent API equivalent to [`Interactivity::occlude_mouse`]. + fn occlude(mut self) -> Self { + self.interactivity().occlude_mouse(); + self + } + + /// Set the bounds of this element as a window control area for the platform window. + /// The fluent API equivalent to [`Interactivity::window_control_area`]. + fn window_control_area(mut self, area: WindowControlArea) -> Self { + self.interactivity().window_control_area(area); + self + } + + /// Block non-scroll mouse interactions with elements behind this element's hitbox. + /// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]. + /// + /// See [`Hitbox::is_hovered`] for details. + fn block_mouse_except_scroll(mut self) -> Self { + self.interactivity().block_mouse_except_scroll(); + self + } + + /// Set the given styles to be applied when this element, specifically, is focused. + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element is inside another element that is focused. + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element is focused via keyboard navigation. + /// This is similar to CSS's `:focus-visible` pseudo-class - it only applies when the element + /// is focused AND the user is navigating via keyboard (not mouse clicks). + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn focus_visible(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_visible_style = Some(Box::new(f(StyleRefinement::default()))); + self + } +} + +/// A trait for elements that want to use the standard GPUI interactivity features +/// that require state. +pub trait StatefulInteractiveElement: InteractiveElement { + /// Set the accessible role for this element. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + fn role(mut self, role: accesskit::Role) -> Self { + debug_assert!( + role != accesskit::Role::GenericContainer, + "GenericContainer is filtered out of the a11y tree and has no effect" + ); + self.interactivity().override_role = Some(role); + self + } + + /// Set the author-provided identifier exposed to accessibility clients. + /// + /// Unlike the GPUI element ID, this value is visible outside the process. + /// Keep it stable and unique within its accessibility tree. + /// AccessKit maps it to platform identifiers where supported, including + /// UIA `AutomationId` on Windows, `AXIdentifier` on macOS, and AT-SPI + /// `AccessibleId` on Linux stacks whose deployed adapter exposes it. + fn accessibility_id(mut self, id: impl Into) -> Self { + self.interactivity().aria.author_id = Some(id.into()); + self + } + + /// Set the accessible label for this element. + fn aria_label(mut self, label: impl Into) -> Self { + self.interactivity().aria.label = Some(label.into()); + self + } + + /// Set the accessible description for this element. Unlike the label (which + /// names the element), the description provides supplementary information + /// that assistive technology announces after the name, role, and value - + /// for example a settings subtitle or a hint. + fn aria_description(mut self, description: impl Into) -> Self { + self.interactivity().aria.description = Some(description.into()); + self + } + + /// Set the keyboard shortcut(s) that activate this element, announced by + /// assistive technology (maps to AccessKit's `keyboard_shortcut`). + /// + /// Note that this does not create a keymap. It simply instructs assistive + /// technology what the keymap is. + fn aria_keyshortcuts(mut self, keyshortcuts: impl Into) -> Self { + self.interactivity().aria.keyshortcuts = Some(keyshortcuts.into()); + self + } + + /// Report this element as the focused node in the accessibility tree, + /// overriding the element that holds real keyboard focus — but only while + /// one of its ancestors actually holds focus. + /// + /// This implements the `aria-activedescendant` pattern for composite + /// widgets that keep keyboard focus on a container (e.g. a menu or + /// listbox) while a child is "selected": set this on the selected child so + /// assistive technology announces and highlights it as focused. + /// + /// The element must also have a [`role`][Self::role] (and an id) so it + /// produces an accessibility node. Unlike the web's container-side + /// `aria-activedescendant`, this is set on the descendant; GPUI honors it + /// only when a focused ancestor is present in the tree, so it is safe to + /// set unconditionally on the selected child — if the container isn't + /// focused, the claim is ignored. + fn aria_active_descendant(mut self) -> Self { + self.interactivity().report_active_descendant_focus = true; + self + } + + /// Contribute synthetic accessibility nodes — nodes that don't correspond + /// to any element — as children of this element's a11y node. For example, + /// text runs describing an editor's text content. + /// + /// The closure is called after this element is prepainted, and only if it + /// contributed a node to the accessibility tree (i.e. it has an id and a + /// [`role`][StatefulInteractiveElement::role]). + /// + /// See [`Element::a11y_synthetic_children`] for details. + fn a11y_synthetic_children( + mut self, + f: impl FnOnce(&mut crate::A11ySubtreeBuilder) + 'static, + ) -> Self { + self.interactivity().a11y_synthetic_children = Some(Box::new(f)); + self + } + + /// Set the selected state for this element. + fn aria_selected(mut self, selected: bool) -> Self { + self.interactivity().aria.selected = Some(selected); + self + } + + /// Set the expanded state for this element. + fn aria_expanded(mut self, expanded: bool) -> Self { + self.interactivity().aria.expanded = Some(expanded); + self + } + + /// Set the toggled state for this element. + fn aria_toggled(mut self, toggled: accesskit::Toggled) -> Self { + self.interactivity().aria.toggled = Some(toggled); + self + } + + /// Set the numeric value for this element. + fn aria_numeric_value(mut self, value: f64) -> Self { + self.interactivity().aria.numeric_value = Some(value); + self + } + + /// Set the step by which assistive technology should expect the numeric + /// value of this element to change (e.g. when incrementing a spin button). + fn aria_numeric_value_step(mut self, step: f64) -> Self { + self.interactivity().aria.numeric_value_step = Some(step); + self + } + + /// Set the string value of this element, e.g. the text content of a simple + /// text input. + fn aria_value(mut self, value: impl Into) -> Self { + self.interactivity().aria.value = Some(value.into()); + self + } + + /// Set the placeholder text reported to assistive technology for this + /// element, shown when a text input is empty. + fn aria_placeholder(mut self, placeholder: impl Into) -> Self { + self.interactivity().aria.placeholder = Some(placeholder.into()); + self + } + + /// Set the minimum numeric value for this element. + fn aria_min_numeric_value(mut self, value: f64) -> Self { + self.interactivity().aria.min_numeric_value = Some(value); + self + } + + /// Set the maximum numeric value for this element. + fn aria_max_numeric_value(mut self, value: f64) -> Self { + self.interactivity().aria.max_numeric_value = Some(value); + self + } + + /// Set the orientation of this element. + fn aria_orientation(mut self, orientation: accesskit::Orientation) -> Self { + self.interactivity().aria.orientation = Some(orientation); + self + } + + /// Set the heading level of this element. + fn aria_level(mut self, level: usize) -> Self { + self.interactivity().aria.level = Some(level); + self + } + + /// Set the position in set of this element. + fn aria_position_in_set(mut self, position: usize) -> Self { + self.interactivity().aria.position_in_set = Some(position); + self + } + + /// Set the size of set for this element. + fn aria_size_of_set(mut self, size: usize) -> Self { + self.interactivity().aria.size_of_set = Some(size); + self + } + + /// Set the row index for this element. + fn aria_row_index(mut self, index: usize) -> Self { + self.interactivity().aria.row_index = Some(index); + self + } + + /// Set the column index for this element. + fn aria_column_index(mut self, index: usize) -> Self { + self.interactivity().aria.column_index = Some(index); + self + } + + /// Set the row count for this element. + fn aria_row_count(mut self, count: usize) -> Self { + self.interactivity().aria.row_count = Some(count); + self + } + + /// Set the column count for this element. + fn aria_column_count(mut self, count: usize) -> Self { + self.interactivity().aria.column_count = Some(count); + self + } + + /// Register a handler for an accessibility action on this element. + /// The handler is called when a screen reader requests the given action. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + #[inline(always)] + fn on_a11y_action( + mut self, + action: accesskit::Action, + listener: impl FnMut(Option<&accesskit::ActionData>, &mut crate::Window, &mut crate::App) + + 'static, + ) -> Self { + self.interactivity() + .a11y_action_listeners + .push((action, Box::new(listener))); + self + } + + /// Set this element to focusable. + fn focusable(mut self) -> Self { + self.interactivity().focusable = true; + self + } + + /// Set the overflow x and y to scroll. + fn overflow_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Set the overflow x to scroll. + fn overflow_x_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self + } + + /// Set the overflow y to scroll. + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + /// Restrict scrolling of this element to the axis of the input gesture. + /// + /// See [`Style::restrict_scroll_to_axis`](crate::Style::restrict_scroll_to_axis) for details. + fn restrict_scroll_to_axis(mut self) -> Self { + self.interactivity().base_style.restrict_scroll_to_axis = Some(true); + self + } + + /// Track the scroll state of this element with the given handle. + fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); + self + } + + /// Track the scroll state of this element with the given handle. + fn anchor_scroll(mut self, scroll_anchor: Option) -> Self { + self.interactivity().scroll_anchor = scroll_anchor; + self + } + + /// Set the given styles to be applied when this element is active. + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().active_style = Some(Box::new(f(StyleRefinement::default()))); + self + } + + /// Set the given styles to be applied when this element's group is active. + fn group_active( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_active_style = Some(GroupStyle { + group: group_name.into(), + style: Box::new(f(StyleRefinement::default())), + }); + self + } + + /// Bind the given callback to click events of this element. + /// The fluent API equivalent to [`Interactivity::on_click`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + #[inline(always)] + fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_click(listener); + self + } + + /// Bind the given callback to non-primary click events of this element. + /// The fluent API equivalent to [`Interactivity::on_aux_click`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_aux_click( + mut self, + listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().on_aux_click(listener); + self + } + + /// On drag initiation, this callback will be used to create a new view to render the dragged value for a + /// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with + /// the [`InteractiveElement::on_drag_move`] API. + /// The callback also has access to the offset of triggering click from the origin of parent element. + /// The fluent API equivalent to [`Interactivity::on_drag`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_drag( + mut self, + value: T, + constructor: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, + ) -> Self + where + Self: Sized, + T: 'static, + W: 'static + Render, + { + self.interactivity().on_drag(value, constructor); + self + } + + /// Registers a callback resolving a payload to offer the platform if a drag started by this + /// element leaves the window. It is invoked at most once per drag gesture, when the pointer + /// exits the viewport. Must be called after [`Self::on_drag`], with the same dragged value + /// type `T`. + /// The fluent API equivalent to [`Interactivity::external_drag_payload`]. + fn external_drag_payload( + mut self, + resolver: impl Fn(&T, &mut Window, &mut App) -> Option + 'static, + ) -> Self + where + Self: Sized, + T: 'static, + { + self.interactivity().external_drag_payload(resolver); + self + } + + /// Bind the given callback on the hover start and end events of this element. Note that the boolean + /// passed to the callback is true when the hover starts and false when it ends. + /// Transitions caused by layout changes under a stationary mouse also invoke the callback. + /// + /// By default, keyboard input suppresses hover until the next mouse move, mouse down, or touch. Set + /// [`HoverListenerMode::InputModalityIndependent`] with [`Self::hover_listener_mode`] to + /// continue hit-testing hover after keyboard input. + /// The fluent API equivalent to [`Interactivity::on_hover`]. + /// + /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. + fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self + where + Self: Sized, + { + self.interactivity().on_hover(listener); + self + } + + /// Sets how [`Self::on_hover`] responds to key presses while the mouse is stationary. + /// This affects only the hover listener, not hover styles or tooltips. The fluent API + /// equivalent to [`Interactivity::hover_listener_mode`]. + fn hover_listener_mode(mut self, mode: HoverListenerMode) -> Self + where + Self: Sized, + { + self.interactivity().hover_listener_mode(mode); + self + } + + /// Constructs a tooltip when the element is hovered or long-pressed. + /// The fluent API equivalent to [`Interactivity::tooltip`]. + fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self + where + Self: Sized, + { + self.interactivity().tooltip(build_tooltip); + self + } + + /// Constructs a tooltip when the element is hovered or long-pressed. + /// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into + /// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]. + fn hoverable_tooltip( + mut self, + build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().hoverable_tooltip(build_tooltip); + self + } + + /// Sets the delay before this element's tooltip is shown on hover. + /// + /// Touch long presses show the tooltip immediately once the gesture is recognized. + /// The fluent API equivalent to [`Interactivity::tooltip_show_delay`]. + fn tooltip_show_delay(mut self, delay: Duration) -> Self + where + Self: Sized, + { + self.interactivity().tooltip_show_delay(delay); + self + } +} + +pub(crate) type MouseDownListener = + Box; +pub(crate) type MouseUpListener = + Box; +pub(crate) type MousePressureListener = + Box; +pub(crate) type MouseMoveListener = + Box; +pub(crate) type MouseExitListener = + Box; + +pub(crate) type FileDropExitListener = + Box; + +pub(crate) type ScrollWheelListener = + Box; + +pub(crate) type PinchListener = + Box; + +pub(crate) type ClickListener = Rc; + +/// Controls how [`StatefulInteractiveElement::on_hover`] responds to key presses while the mouse +/// is stationary. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum HoverListenerMode { + /// Use input-modality-aware hit testing. Keyboard input suppresses hover until the mouse moves + /// again, unless pointer capture or an active mouse-down interaction keeps the listener hovered. + #[default] + InputModalityAware, + /// Use hit testing even when the last input was from the keyboard. This changes only + /// keyboard-modality filtering; all other [`StatefulInteractiveElement::on_hover`] behavior + /// remains unchanged. + InputModalityIndependent, +} + +impl HoverListenerMode { + fn is_hovered(self, hitbox: &Hitbox, window: &Window) -> bool { + match self { + Self::InputModalityAware => hitbox.is_hovered(window), + Self::InputModalityIndependent => hitbox.id.is_hovered_ignoring_last_input(window), + } + } +} + +pub(crate) struct DragListener { + value: Arc, + render: Box, &mut Window, &mut App) -> AnyView + 'static>, + external_payload: Option, +} + +type ExternalDragPayloadResolver = + Box Option + 'static>; + +type DropListener = Box; + +type CanDropPredicate = Box bool + 'static>; + +pub(crate) struct TooltipBuilder { + build: Rc AnyView + 'static>, + hoverable: bool, +} + +pub(crate) type KeyDownListener = + Box; + +pub(crate) type KeyUpListener = + Box; + +pub(crate) type ModifiersChangedListener = + Box; + +pub(crate) type ActionListener = + Box; + +/// Construct a new [`Div`] element +#[track_caller] +pub fn div() -> Div { + Div { + interactivity: Interactivity::new(), + children: SmallVec::default(), + prepaint_listener: None, + image_cache: None, + prepaint_order_fn: None, + } +} + +/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[StackSafe; 2]>, + prepaint_listener: Option>, &mut Window, &mut App) + 'static>>, + image_cache: Option>, + prepaint_order_fn: Option SmallVec<[usize; 8]>>>, +} + +impl Div { + /// Add a listener to be called when the children of this `Div` are prepainted. + /// This allows you to store the [`Bounds`] of the children for later use. + pub fn on_children_prepainted( + mut self, + listener: impl Fn(Vec>, &mut Window, &mut App) + 'static, + ) -> Self { + self.prepaint_listener = Some(Box::new(listener)); + self + } + + /// Add an image cache at the location of this div in the element tree. + pub fn image_cache(mut self, cache: impl ImageCacheProvider) -> Self { + self.image_cache = Some(Box::new(cache)); + self + } + + /// Specify a function that determines the order in which children are prepainted. + /// + /// The function is called at prepaint time and should return a vector of child indices + /// in the desired prepaint order. Each index should appear exactly once. + /// + /// This is useful when the prepaint of one child affects state that another child reads. + /// For example, in split editor views, the editor with an autoscroll request should + /// be prepainted first so its scroll position update is visible to the other editor. + pub fn with_dynamic_prepaint_order( + mut self, + order_fn: impl Fn(&mut Window, &mut App) -> SmallVec<[usize; 8]> + 'static, + ) -> Self { + self.prepaint_order_fn = Some(Box::new(order_fn)); + self + } +} + +/// A frame state for a `Div` element, which contains layout IDs for its children. +/// +/// This struct is used internally by the `Div` element to manage the layout state of its children +/// during the UI update cycle. It holds a small vector of `LayoutId` values, each corresponding to +/// a child element of the `Div`. These IDs are used to query the layout engine for the computed +/// bounds of the children after the layout phase is complete. +pub struct DivFrameState { + child_layout_ids: SmallVec<[LayoutId; 2]>, +} + +/// Interactivity state displayed an manipulated in the inspector. +#[derive(Clone)] +pub struct DivInspectorState { + /// The inspected element's base style. This is used for both inspecting and modifying the + /// state. In the future it will make sense to separate the read and write, possibly tracking + /// the modifications. + #[cfg(any(feature = "inspector", debug_assertions))] + pub base_style: Box, + /// Inspects the bounds of the element. + pub bounds: Bounds, + /// Size of the children of the element, or `bounds.size` if it has no children. + pub content_size: Size, +} + +impl Styled for Div { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Div { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl ParentElement for Div { + fn extend(&mut self, elements: impl IntoIterator) { + #[cfg(feature = "stacker")] + self.children + .extend(elements.into_iter().map(StackSafe::new)); + #[cfg(not(feature = "stacker"))] + self.children.extend(elements); + } +} + +impl Element for Div { + type RequestLayoutState = DivFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + self.interactivity.source_location() + } + + fn a11y_role(&self) -> Option { + // Nodes with `GenericContainer` should never be reported to accesskit. + // Equivalent to an HTML div with no role. + self.interactivity + .override_role + .filter(|role| *role != accesskit::Role::GenericContainer) + } + + fn write_a11y_info(&self, node: &mut accesskit::Node) { + self.interactivity.write_a11y_info(node); + } + + fn a11y_synthetic_children( + &mut self, + _prepaint: &mut Self::PrepaintState, + builder: &mut crate::A11ySubtreeBuilder, + ) { + if let Some(f) = self.interactivity.a11y_synthetic_children.take() { + f(builder); + } + } + + #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut child_layout_ids = SmallVec::new(); + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + let layout_id = window.with_image_cache(image_cache, |window| { + self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| { + window.with_text_style(style.text_style().cloned(), |window| { + child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + window.request_layout(style, child_layout_ids.iter().copied(), cx) + }) + }, + ) + }); + + (layout_id, DivFrameState { child_layout_ids }) + } + + #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + let has_prepaint_listener = self.prepaint_listener.is_some(); + let mut children_bounds = Vec::with_capacity(if has_prepaint_listener { + request_layout.child_layout_ids.len() + } else { + 0 + }); + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + if let Some(handle) = self.interactivity.scroll_anchor.as_ref() { + *handle.last_origin.borrow_mut() = bounds.origin - window.element_offset(); + } + let content_size = if request_layout.child_layout_ids.is_empty() { + bounds.size + } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { + let mut state = scroll_handle.0.borrow_mut(); + state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = window.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.bottom_right()); + state.child_bounds.push(child_bounds); + } + (child_max - child_min).into() + } else { + for child_layout_id in &request_layout.child_layout_ids { + let child_bounds = window.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.bottom_right()); + + if has_prepaint_listener { + children_bounds.push(child_bounds); + } + } + (child_max - child_min).into() + }; + + if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { + scroll_handle.scroll_to_active_item(); + } + + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + content_size, + window, + cx, + |style, scroll_offset, hitbox, window, cx| { + // skip children + if style.display == Display::None { + return hitbox; + } + + window.with_image_cache(image_cache, |window| { + window.with_element_offset(scroll_offset, |window| { + if let Some(order_fn) = &self.prepaint_order_fn { + let order = order_fn(window, cx); + for idx in order { + if let Some(child) = self.children.get_mut(idx) { + child.prepaint(window, cx); + } + } + } else { + for child in &mut self.children { + child.prepaint(window, cx); + } + } + }); + + if let Some(listener) = self.prepaint_listener.as_ref() { + listener(children_bounds, window, cx); + } + }); + + hitbox + }, + ) + } + + #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) { + let image_cache = self + .image_cache + .as_mut() + .map(|provider| provider.provide(window, cx)); + + window.with_image_cache(image_cache, |window| { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + // skip children + if style.display == Display::None { + return; + } + + for child in &mut self.children { + child.paint(window, cx); + } + }, + ) + }); + } +} + +impl IntoElement for Div { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +#[derive(Default)] +pub(crate) struct AriaProperties { + pub(crate) author_id: Option, + pub(crate) label: Option, + pub(crate) description: Option, + pub(crate) keyshortcuts: Option, + pub(crate) selected: Option, + pub(crate) expanded: Option, + pub(crate) toggled: Option, + pub(crate) numeric_value: Option, + pub(crate) min_numeric_value: Option, + pub(crate) max_numeric_value: Option, + pub(crate) numeric_value_step: Option, + pub(crate) value: Option, + pub(crate) placeholder: Option, + pub(crate) orientation: Option, + pub(crate) level: Option, + pub(crate) position_in_set: Option, + pub(crate) size_of_set: Option, + pub(crate) row_index: Option, + pub(crate) column_index: Option, + pub(crate) row_count: Option, + pub(crate) column_count: Option, +} + +/// The interactivity struct. Powers all of the general-purpose +/// interactivity in the `Div` element. +#[derive(Default)] +pub struct Interactivity { + /// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click. + pub element_id: Option, + /// Whether the element was clicked. This will only be present after layout. + pub active: Option, + /// Whether the element was hovered. This will only be present after paint if an hitbox + /// was created for the interactive element. + pub hovered: Option, + pub(crate) tooltip_id: Option, + pub(crate) content_size: Size, + pub(crate) key_context: Option, + pub(crate) focusable: bool, + pub(crate) tracked_focus_handle: Option, + pub(crate) tracked_scroll_handle: Option, + pub(crate) scroll_anchor: Option, + pub(crate) scroll_offset: Option>>>, + pub(crate) ongoing_scroll: Option>>, + pub(crate) group: Option, + /// The base style of the element, before any modifications are applied + /// by focus, active, etc. + pub base_style: Box, + pub(crate) focus_style: Option>, + pub(crate) in_focus_style: Option>, + pub(crate) focus_visible_style: Option>, + pub(crate) hover_style: Option>, + pub(crate) group_hover_style: Option, + pub(crate) active_style: Option>, + pub(crate) group_active_style: Option, + pub(crate) drag_over_styles: Vec<( + TypeId, + Box StyleRefinement>, + )>, + pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, + pub(crate) mouse_down_listeners: Vec, + pub(crate) mouse_up_listeners: Vec, + pub(crate) mouse_pressure_listeners: Vec, + pub(crate) mouse_move_listeners: Vec, + pub(crate) mouse_exit_listeners: Vec, + pub(crate) file_drop_exit_listeners: Vec, + pub(crate) scroll_wheel_listeners: Vec, + pub(crate) pinch_listeners: Vec, + pub(crate) key_down_listeners: Vec, + pub(crate) key_up_listeners: Vec, + pub(crate) modifiers_changed_listeners: Vec, + pub(crate) action_listeners: Vec<(TypeId, ActionListener)>, + pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, + pub(crate) can_drop_predicate: Option, + pub(crate) click_listeners: Vec, + pub(crate) aux_click_listeners: Vec, + pub(crate) drag_listener: Option, + pub(crate) hover_listener: Option>, + pub(crate) hover_listener_mode: HoverListenerMode, + pub(crate) tooltip_builder: Option, + pub(crate) tooltip_show_delay: Option, + pub(crate) window_control: Option, + pub(crate) hitbox_behavior: HitboxBehavior, + pub(crate) tab_index: Option, + pub(crate) tab_group: bool, + pub(crate) tab_stop: bool, + + pub(crate) a11y_action_listeners: + Vec<(accesskit::Action, crate::window::a11y::A11yActionListener)>, + pub(crate) a11y_synthetic_children: Option>, + pub(crate) report_active_descendant_focus: bool, + pub(crate) override_role: Option, + pub(crate) aria: AriaProperties, + + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) source_location: Option<&'static core::panic::Location<'static>>, + + #[cfg(any(test, all(debug_assertions, feature = "test-support")))] + pub(crate) debug_selector: Option, +} + +impl Interactivity { + /// Layout this element according to this interactivity state's configured styles + pub fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(Style, &mut Window, &mut App) -> LayoutId, + ) -> LayoutId { + #[cfg(any(feature = "inspector", debug_assertions))] + window.with_inspector_state( + _inspector_id, + cx, + |inspector_state: &mut Option, _window| { + if let Some(inspector_state) = inspector_state { + self.base_style = inspector_state.base_style.clone(); + } else { + *inspector_state = Some(DivInspectorState { + base_style: self.base_style.clone(), + bounds: Default::default(), + content_size: Default::default(), + }) + } + }, + ); + + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + if let Some(element_state) = element_state.as_ref() + && cx.has_active_drag() + { + if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() { + *pending_mouse_down.borrow_mut() = None; + } + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + *clicked_state.borrow_mut() = ElementClickedState::default(); + } + } + + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable + && self.tracked_focus_handle.is_none() + && let Some(element_state) = element_state.as_mut() + { + let mut handle = element_state + .focus_handle + .get_or_insert_with(|| cx.focus_handle()) + .clone() + .tab_stop(self.tab_stop); + + if let Some(index) = self.tab_index { + handle = handle.tab_index(index); + } + + self.tracked_focus_handle = Some(handle); + } + + if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() { + let scroll_handle_state = scroll_handle.0.borrow(); + self.scroll_offset = Some(scroll_handle_state.offset.clone()); + self.ongoing_scroll = Some(scroll_handle_state.ongoing_scroll.clone()); + } else if (self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll)) + && let Some(element_state) = element_state.as_mut() + { + self.scroll_offset = Some( + element_state + .scroll_offset + .get_or_insert_with(Rc::default) + .clone(), + ); + self.ongoing_scroll = Some( + element_state + .ongoing_scroll + .get_or_insert_with(|| Rc::new(RefCell::new(OngoingScroll::default()))) + .clone(), + ); + } + + let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); + let layout_id = f(style, window, cx); + (layout_id, element_state) + }, + ) + } + + /// Commit the bounds of this element according to this interactivity state's configured styles. + pub fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + content_size: Size, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(&Style, Point, Option, &mut Window, &mut App) -> R, + ) -> R { + self.content_size = content_size; + + #[cfg(any(feature = "inspector", debug_assertions))] + window.with_inspector_state( + _inspector_id, + cx, + |inspector_state: &mut Option, _window| { + if let Some(inspector_state) = inspector_state { + inspector_state.bounds = bounds; + inspector_state.content_size = content_size; + } + }, + ); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + window.set_focus_handle(focus_handle, cx); + + if window.a11y.is_active() { + if let Some(global_id) = global_id { + let node_id = global_id.accesskit_node_id(); + window.a11y.set_focusable(node_id, focus_handle.id); + if focus_handle.is_focused(window) { + window.a11y.set_focus(node_id); + } + } else if focus_handle.is_focused(window) { + // Focusable, but with no element id it can't have an + // accessibility node, so screen readers fall back to the + // whole window. + window + .a11y + .note_focus_without_node(focus_handle.id, "it has no element id"); + } + } + } + + if self.report_active_descendant_focus && window.a11y.is_active() { + if let Some(global_id) = global_id { + window + .a11y + .set_active_descendant(global_id.accesskit_node_id()); + } + } + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(None, element_state.as_mut(), window, cx); + + if let Some(element_state) = element_state.as_mut() { + if let Some(clicked_state) = element_state.clicked_state.as_ref() { + let clicked_state = clicked_state.borrow(); + self.active = Some(clicked_state.element); + } + if self.hover_style.is_some() || self.group_hover_style.is_some() { + element_state + .hover_state + .get_or_insert_with(Default::default); + } + if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { + if self.tooltip_builder.is_some() { + self.tooltip_id = set_tooltip_on_window(active_tooltip, window); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + if let Some(long_press_tooltip_active) = + element_state.long_press_tooltip_active.as_ref() + { + long_press_tooltip_active.set(false); + } + element_state.active_tooltip.take(); + } + } + } + + window.with_text_style(style.text_style().cloned(), |window| { + window.with_content_mask( + style.overflow_mask(bounds, window.rem_size()), + |window| { + let hitbox = if self.should_insert_hitbox(&style, window, cx) { + Some(window.insert_hitbox(bounds, self.hitbox_behavior)) + } else { + None + }; + + let scroll_offset = + self.clamp_scroll_position(bounds, &style, window, cx); + let result = f(&style, scroll_offset, hitbox, window, cx); + (result, element_state) + }, + ) + }) + }, + ) + } + + fn should_insert_hitbox(&self, style: &Style, window: &Window, cx: &App) -> bool { + self.hitbox_behavior != HitboxBehavior::Normal + || self.window_control.is_some() + || style.mouse_cursor.is_some() + || self.group.is_some() + || self.scroll_offset.is_some() + || self.tracked_focus_handle.is_some() + || self.hover_style.is_some() + || self.group_hover_style.is_some() + || self.hover_listener.is_some() + || !self.mouse_up_listeners.is_empty() + || !self.mouse_pressure_listeners.is_empty() + || !self.mouse_down_listeners.is_empty() + || !self.mouse_move_listeners.is_empty() + || !self.mouse_exit_listeners.is_empty() + || !self.file_drop_exit_listeners.is_empty() + || !self.click_listeners.is_empty() + || !self.aux_click_listeners.is_empty() + || !self.scroll_wheel_listeners.is_empty() + || self.has_pinch_listeners() + || self.drag_listener.is_some() + || !self.drop_listeners.is_empty() + || !self.drag_over_styles.is_empty() + || self.tooltip_builder.is_some() + || window.is_inspector_picking(cx) + } + + fn clamp_scroll_position( + &self, + bounds: Bounds, + style: &Style, + window: &mut Window, + _cx: &mut App, + ) -> Point { + fn round_to_two_decimals(pixels: Pixels) -> Pixels { + const ROUNDING_FACTOR: f32 = 100.0; + (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR + } + + if let Some(scroll_offset) = self.scroll_offset.as_ref() { + let mut scroll_to_bottom = false; + let mut tracked_scroll_handle = self + .tracked_scroll_handle + .as_ref() + .map(|handle| handle.0.borrow_mut()); + if let Some(mut scroll_handle_state) = tracked_scroll_handle.as_deref_mut() { + scroll_handle_state.overflow = style.overflow; + scroll_to_bottom = mem::take(&mut scroll_handle_state.scroll_to_bottom); + } + + let rem_size = window.rem_size(); + // Taffy lays the box out with the padding snapped to the device pixel + // grid (`to_taffy`); recomputed unsnapped, e.g. py_1 at a fractional + // rem size, it exceeds `bounds` and leaves the box scrollable by the + // sub-pixel difference. + let padding = style + .padding + .to_pixels(bounds.size.into(), rem_size) + .map(|edge| window.pixel_snap(*edge)); + let padding_size = size(padding.left + padding.right, padding.top + padding.bottom); + // The floating point values produced by Taffy and ours often vary + // slightly after ~5 decimal places. This can lead to cases where after + // subtracting these, the container becomes scrollable for less than + // 0.00000x pixels. As we generally don't benefit from a precision that + // high for the maximum scroll, we round the scroll max to 2 decimal + // places here. + let padded_content_size = self.content_size + padding_size; + let scroll_max = Point::from(padded_content_size - bounds.size) + .map(round_to_two_decimals) + .max(&Default::default()); + // Clamp scroll offset in case scroll max is smaller now (e.g., if children + // were removed or the bounds became larger). + let mut scroll_offset = scroll_offset.borrow_mut(); + + scroll_offset.x = scroll_offset.x.clamp(-scroll_max.x, px(0.)); + if scroll_to_bottom { + scroll_offset.y = -scroll_max.y; + } else { + scroll_offset.y = scroll_offset.y.clamp(-scroll_max.y, px(0.)); + } + + if let Some(mut scroll_handle_state) = tracked_scroll_handle { + scroll_handle_state.max_offset = scroll_max; + scroll_handle_state.bounds = bounds; + } + + *scroll_offset + } else { + Point::default() + } + } + + /// Paint this element according to this interactivity state's configured styles + /// and bind the element's mouse and keyboard events. + /// + /// content_size is the size of the content of the element, which may be larger than the + /// element's bounds if the element is scrollable. + /// + /// the final computed style will be passed to the provided function, along + /// with the current scroll offset + pub fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + hitbox: Option<&Hitbox>, + window: &mut Window, + cx: &mut App, + f: impl FnOnce(&Style, &mut Window, &mut App), + ) { + self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(window)); + window.with_optional_element_state::( + global_id, + |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + + let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); + + #[cfg(any(test, all(debug_assertions, feature = "test-support")))] + if let Some(debug_selector) = &self.debug_selector { + window + .next_frame + .record_debug_bounds(debug_selector.clone(), bounds); + } + + self.paint_hover_group_handler(window, cx); + + if style.visibility == Visibility::Hidden { + return ((), element_state); + } + + let mut tab_group = None; + if self.tab_group { + tab_group = self.tab_index; + } + + window.with_element_opacity(style.opacity, |window| { + style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| { + window.with_text_style(style.text_style().cloned(), |window| { + window.with_content_mask( + style.overflow_mask(bounds, window.rem_size()), + |window| { + window.with_tab_group(tab_group, |window| { + // Register the container's own focus handle *inside* its + // tab group, so that focusing the container and then + // calling `focus_next` descends into this group's first + // item. Inserting it before `with_tab_group` would give the + // container a shallower tab path than its children; with + // sibling groups every container would then sort ahead of + // every item, and `focus_next` from a container would jump + // to the first item in the whole window instead of its own. + if let Some(focus_handle) = &self.tracked_focus_handle { + window.next_frame.tab_stops.insert(focus_handle); + } + if let Some(hitbox) = hitbox { + #[cfg(debug_assertions)] + self.paint_debug_info( + global_id, hitbox, &style, window, cx, + ); + + if let Some(drag) = cx.active_drag.as_ref() { + if let Some(mouse_cursor) = drag.cursor_style { + window.set_window_cursor_style(mouse_cursor); + } + } else { + if let Some(mouse_cursor) = style.mouse_cursor { + window.set_cursor_style(mouse_cursor, hitbox); + } + } + + if let Some(group) = self.group.clone() { + GroupHitboxes::push(group, hitbox.id, cx); + } + + if let Some(area) = self.window_control { + window.insert_window_control_hitbox( + area, + hitbox.clone(), + ); + } + + self.paint_mouse_listeners( + hitbox, + element_state.as_mut(), + window, + cx, + ); + self.paint_scroll_listener(hitbox, &style, window, cx); + } + + self.paint_keyboard_listeners(window, cx); + + if window.a11y.is_active() { + if let Some(global_id) = global_id { + if !self.a11y_action_listeners.is_empty() { + let node_id = global_id.accesskit_node_id(); + for (action, listener) in + self.a11y_action_listeners.drain(..) + { + window.on_a11y_action( + node_id, action, listener, + ); + } + } + } + } + + f(&style, window, cx); + + if let Some(_hitbox) = hitbox { + #[cfg(any(feature = "inspector", debug_assertions))] + window.insert_inspector_hitbox( + _hitbox.id, + _inspector_id, + cx, + ); + + if let Some(group) = self.group.as_ref() { + GroupHitboxes::pop(group, cx); + } + } + }) + }, + ); + }); + }); + }); + + ((), element_state) + }, + ); + } + + #[cfg(debug_assertions)] + fn paint_debug_info( + &self, + global_id: Option<&GlobalElementId>, + hitbox: &Hitbox, + style: &Style, + window: &mut Window, + cx: &mut App, + ) { + use crate::{BorderStyle, TextAlign}; + + if let Some(global_id) = global_id + && (style.debug || style.debug_below || cx.has_global::()) + && hitbox.is_hovered(window) + { + const FONT_SIZE: crate::Pixels = crate::Pixels(10.); + let element_id = format!("{global_id:?}"); + let str_len = element_id.len(); + + let render_debug_text = |window: &mut Window| { + if let Some(text) = window + .text_system() + .shape_text( + element_id.into(), + FONT_SIZE, + &[window.text_style().to_run(str_len)], + None, + None, + ) + .ok() + .and_then(|mut text| text.pop()) + { + text.paint(hitbox.origin, FONT_SIZE, TextAlign::Left, None, window, cx) + .ok(); + + let text_bounds = crate::Bounds { + origin: hitbox.origin, + size: text.size(FONT_SIZE), + }; + if let Some(source_location) = self.source_location + && text_bounds.contains(&window.mouse_position()) + && window.modifiers().secondary() + { + let secondary_held = window.modifiers().secondary(); + window.on_key_event({ + move |e: &crate::ModifiersChangedEvent, _phase, window, _cx| { + if e.modifiers.secondary() != secondary_held + && text_bounds.contains(&window.mouse_position()) + { + window.refresh(); + } + } + }); + + let was_hovered = hitbox.is_hovered(window); + let current_view = window.current_view(); + window.on_mouse_event({ + let hitbox = hitbox.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Capture { + let hovered = hitbox.is_hovered(window); + if hovered != was_hovered { + cx.notify(current_view) + } + } + } + }); + + window.on_mouse_event({ + let hitbox = hitbox.clone(); + move |e: &crate::MouseDownEvent, phase, window, cx| { + if text_bounds.contains(&e.position) + && phase.capture() + && hitbox.is_hovered(window) + { + cx.stop_propagation(); + let Ok(dir) = std::env::current_dir() else { + return; + }; + + eprintln!( + "This element was created at:\n{}:{}:{}", + dir.join(source_location.file()).to_string_lossy(), + source_location.line(), + source_location.column() + ); + } + } + }); + window.paint_quad(crate::outline( + crate::Bounds { + origin: hitbox.origin + + crate::point(crate::px(0.), FONT_SIZE - px(2.)), + size: crate::Size { + width: text_bounds.size.width, + height: crate::px(1.), + }, + }, + crate::red(), + BorderStyle::default(), + )) + } + } + }; + + window.with_text_style( + Some(crate::TextStyleRefinement { + color: Some(crate::red()), + line_height: Some(FONT_SIZE.into()), + background_color: Some(crate::white()), + ..Default::default() + }), + render_debug_text, + ) + } + } + + fn paint_mouse_listeners( + &mut self, + hitbox: &Hitbox, + element_state: Option<&mut InteractiveElementState>, + window: &mut Window, + cx: &mut App, + ) { + let is_focused = self + .tracked_focus_handle + .as_ref() + .map(|handle| handle.is_focused(window)) + .unwrap_or(false); + + // If this element can be focused, register a mouse down listener + // that will automatically transfer focus when hitting the element. + // This behavior can be suppressed by using `cx.prevent_default()`. + if let Some(focus_handle) = self.tracked_focus_handle.clone() { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && !window.default_prevented() + { + window.focus(&focus_handle, cx); + // If there is a parent that is also focusable, prevent it + // from transferring focus because we already did so. + window.prevent_default(); + } + }); + } + + for listener in self.mouse_down_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_up_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_pressure_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MousePressureEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_move_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.mouse_exit_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseExitEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.file_drop_exit_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &FileDropEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.scroll_wheel_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + for listener in self.pinch_listeners.drain(..) { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &PinchEvent, phase, window, cx| { + listener(event, phase, &hitbox, window, cx); + }) + } + + if self.hover_style.is_some() + || self.base_style.mouse_cursor.is_some() + || cx.active_drag.is_some() && !self.drag_over_styles.is_empty() + { + let hitbox = hitbox.clone(); + let hover_state = self.hover_style.as_ref().and_then(|_| { + element_state + .as_ref() + .and_then(|state| state.hover_state.as_ref()) + .cloned() + }); + let current_view = window.current_view(); + + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + let hovered = hitbox.is_hovered(window); + let was_hovered = hover_state + .as_ref() + .is_some_and(|state| state.borrow().element); + if phase == DispatchPhase::Capture && hovered != was_hovered { + if let Some(hover_state) = &hover_state { + hover_state.borrow_mut().element = hovered; + cx.notify(current_view); + } + } + }); + } + + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { + let hover_state = element_state + .as_ref() + .and_then(|element| element.hover_state.as_ref()) + .cloned(); + let current_view = window.current_view(); + + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + let group_hovered = group_hitbox_id.is_hovered(window); + let was_group_hovered = hover_state + .as_ref() + .is_some_and(|state| state.borrow().group); + if phase == DispatchPhase::Capture && group_hovered != was_group_hovered { + if let Some(hover_state) = &hover_state { + hover_state.borrow_mut().group = group_hovered; + cx.notify(current_view); + } + } + }); + } + } + + let drag_cursor_style = self.base_style.as_ref().mouse_cursor; + + let mut drag_listener = mem::take(&mut self.drag_listener); + let drop_listeners = mem::take(&mut self.drop_listeners); + let click_listeners = mem::take(&mut self.click_listeners); + let aux_click_listeners = mem::take(&mut self.aux_click_listeners); + let can_drop_predicate = mem::take(&mut self.can_drop_predicate); + + if !drop_listeners.is_empty() { + let hitbox = hitbox.clone(); + window.on_mouse_event({ + move |_: &MouseUpEvent, phase, window, cx| { + if let Some(drag) = &cx.active_drag + && phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + { + let drag_state_type = drag.value.as_ref().type_id(); + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + + let mut can_drop = true; + if let Some(predicate) = &can_drop_predicate { + can_drop = predicate(drag.value.as_ref(), window, cx); + } + + if can_drop { + listener(drag.value.as_ref(), window, cx); + window.refresh(); + cx.stop_propagation(); + } + } + } + } + } + }); + } + + if let Some(element_state) = element_state { + if !click_listeners.is_empty() + || !aux_click_listeners.is_empty() + || drag_listener.is_some() + { + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + + let pending_keyboard_down = element_state + .pending_keyboard_down + .get_or_insert_with(Default::default) + .clone(); + + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + + window.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + let has_aux_click_listeners = !aux_click_listeners.is_empty(); + move |event: &MouseDownEvent, phase, window, _cx| { + if phase == DispatchPhase::Bubble + && (event.button == MouseButton::Left || has_aux_click_listeners) + && hitbox.is_hovered(window) + { + *pending_mouse_down.borrow_mut() = Some(event.clone()); + window.refresh(); + } + } + }); + + window.on_mouse_event({ + let pending_mouse_down = pending_mouse_down.clone(); + let hitbox = hitbox.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Capture { + return; + } + + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if let Some(mouse_down) = pending_mouse_down.clone() + && !cx.has_active_drag() + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + && let Some(listener) = drag_listener.take() + && mouse_down.button == MouseButton::Left + { + *clicked_state.borrow_mut() = ElementClickedState::default(); + let cursor_offset = event.position - hitbox.origin; + let drag = (listener.render)( + listener.value.as_ref(), + cursor_offset, + window, + cx, + ); + let external_payload_source = + listener.external_payload.map(|external_payload| { + let value = listener.value.clone(); + Box::new(move |window: &mut Window, cx: &mut App| { + external_payload(value.as_ref(), window, cx) + }) + as ExternalDragPayloadSource + }); + cx.active_drag = Some(AnyDrag { + view: drag, + value: listener.value, + cursor_offset, + cursor_style: drag_cursor_style, + external_payload_source, + }); + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); + } + } + }); + + if is_focused { + // Record the focus generation at which an enter/space key + // down event happened on this element. The next key up + // event will be mapped to a click event if both of the + // following are true: + // - no other key events happen in between + // - the focus generation is the same (implying focus did not move) + // + // This design avoids an ABA problem that happens if you + // store the focus handle that registered the keypress. + window.on_key_event({ + let pending_keyboard_down = pending_keyboard_down.clone(); + move |event: &KeyDownEvent, phase, window, _cx| { + if phase.bubble() && !window.default_prevented() { + let stroke = &event.keystroke; + let is_activation_key = (stroke.key.eq("enter") + || stroke.key.eq("space")) + && !stroke.modifiers.modified(); + *pending_keyboard_down.borrow_mut() = + is_activation_key.then_some(window.focus_generation); + } + } + }); + + // Press enter, space to trigger click, when the element is focused. + window.on_key_event({ + let click_listeners = click_listeners.clone(); + let hitbox = hitbox.clone(); + move |event: &KeyUpEvent, phase, window, cx| { + if phase.bubble() && !window.default_prevented() { + let stroke = &event.keystroke; + let keyboard_button = if stroke.key.eq("enter") { + Some(KeyboardButton::Enter) + } else if stroke.key.eq("space") { + Some(KeyboardButton::Space) + } else { + None + }; + + if let Some(button) = keyboard_button + && !stroke.modifiers.modified() + { + let pending = + std::mem::take(&mut *pending_keyboard_down.borrow_mut()); + if pending != Some(window.focus_generation) { + return; + } + + let click_event = ClickEvent::Keyboard(KeyboardClickEvent { + button, + bounds: hitbox.bounds, + }); + + for listener in &click_listeners { + listener(&click_event, window, cx); + } + } else { + // Releasing any other key mid-press means + // this isn't a clean activation, so cancel + // the pending keydown. + *pending_keyboard_down.borrow_mut() = None; + } + } + } + }); + } + + window.on_mouse_event({ + let mut captured_mouse_down = None; + let hitbox = hitbox.clone(); + move |event: &MouseUpEvent, phase, window, cx| match phase { + // Clear the pending mouse down during the capture phase, + // so that it happens even if another event handler stops + // propagation. + DispatchPhase::Capture => { + let mut pending_mouse_down = pending_mouse_down.borrow_mut(); + if pending_mouse_down.is_some() && hitbox.is_hovered(window) { + captured_mouse_down = pending_mouse_down.take(); + window.refresh(); + } else if pending_mouse_down.is_some() { + // Clear the pending mouse down event (without firing click handlers) + // if the hitbox is not being hovered. + // This avoids dragging elements that changed their position + // immediately after being clicked. + // See https://github.com/zed-industries/zed/issues/24600 for more details + pending_mouse_down.take(); + window.refresh(); + } + } + // Fire click handlers during the bubble phase. + DispatchPhase::Bubble => { + if let Some(mouse_down) = captured_mouse_down.take() { + let btn = mouse_down.button; + + let mouse_click = ClickEvent::Mouse(MouseClickEvent { + down: mouse_down, + up: event.clone(), + }); + + match btn { + MouseButton::Left => { + for listener in &click_listeners { + listener(&mouse_click, window, cx); + } + } + _ => { + for listener in &aux_click_listeners { + listener(&mouse_click, window, cx); + } + } + } + } + } + } + }); + } + + if let Some(hover_listener) = self.hover_listener.take() { + let was_hovered = element_state + .hover_listener_state + .get_or_insert_with(Default::default) + .clone(); + let has_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + let hover_listener = Rc::new(hover_listener); + let hover_listener_state = was_hovered.clone(); + let update_hover = move |is_hovered: bool, window: &mut Window, cx: &mut App| { + let mut was_hovered = hover_listener_state.borrow_mut(); + if is_hovered != *was_hovered { + *was_hovered = is_hovered; + drop(was_hovered); + hover_listener(&is_hovered, window, cx); + } + }; + let hover_listener_mode = self.hover_listener_mode; + + if has_mouse_down.borrow().is_none() { + let is_hovered = + !cx.has_active_drag() && hover_listener_mode.is_hovered(hitbox, window); + if is_hovered != *was_hovered.borrow() { + let update_hover = update_hover.clone(); + window.defer(cx, move |window, cx| { + update_hover(is_hovered, window, cx); + }); + } + } + + window.on_mouse_event({ + let update_hover = update_hover.clone(); + let hitbox = hitbox.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + let is_hovered = has_mouse_down.borrow().is_none() + && !cx.has_active_drag() + && hover_listener_mode.is_hovered(&hitbox, window); + update_hover(is_hovered, window, cx); + } + } + }); + + // The pointer can leave the window without a final MouseMove, so also + // clear hover on MouseExited. + window.on_mouse_event(move |_: &MouseExitEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble { + update_hover(false, window, cx); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); + let long_press_tooltip_active = element_state + .long_press_tooltip_active + .get_or_insert_with(Default::default) + .clone(); + + let tooltip_is_hoverable = tooltip_builder.hoverable; + let build_tooltip = Rc::new(move |window: &mut Window, cx: &mut App| { + Some(((tooltip_builder.build)(window, cx), tooltip_is_hoverable)) + }); + // Use bounds instead of testing hitbox since this is called during prepaint. + let check_is_hovered_during_prepaint = Rc::new({ + let pending_mouse_down = pending_mouse_down.clone(); + let source_bounds = hitbox.bounds; + move |window: &Window| { + !window.last_input_was_keyboard() + && pending_mouse_down.borrow().is_none() + && source_bounds.contains(&window.mouse_position()) + } + }); + let check_is_hovered = Rc::new({ + let hitbox = hitbox.clone(); + move |window: &Window| { + pending_mouse_down.borrow().is_none() && hitbox.is_hovered(window) + } + }); + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + check_is_hovered_during_prepaint, + long_press_tooltip_active, + self.tooltip_show_delay, + window, + ); + } + + // We unconditionally bind both the mouse up and mouse down active state handlers + // Because we might not get a chance to render a frame before the mouse up event arrives. + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); + + { + let active_state = active_state.clone(); + window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| { + if phase == DispatchPhase::Capture && active_state.borrow().is_clicked() { + *active_state.borrow_mut() = ElementClickedState::default(); + window.refresh(); + } + }); + } + + { + let active_group_hitbox = self + .group_active_style + .as_ref() + .and_then(|group_active| GroupHitboxes::get(&group_active.group, cx)); + let hitbox = hitbox.clone(); + window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| { + if phase == DispatchPhase::Bubble && !window.default_prevented() { + let group_hovered = active_group_hitbox + .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); + let element_hovered = hitbox.is_hovered(window); + if group_hovered || element_hovered { + *active_state.borrow_mut() = ElementClickedState { + group: group_hovered, + element: element_hovered, + }; + window.refresh(); + } + } + }); + } + } + } + + fn paint_keyboard_listeners(&mut self, window: &mut Window, _cx: &mut App) { + let key_down_listeners = mem::take(&mut self.key_down_listeners); + let key_up_listeners = mem::take(&mut self.key_up_listeners); + let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners); + let action_listeners = mem::take(&mut self.action_listeners); + if let Some(context) = self.key_context.clone() { + window.set_key_context(context); + } + + for listener in key_down_listeners { + window.on_key_event(move |event: &KeyDownEvent, phase, window, cx| { + listener(event, phase, window, cx); + }) + } + + for listener in key_up_listeners { + window.on_key_event(move |event: &KeyUpEvent, phase, window, cx| { + listener(event, phase, window, cx); + }) + } + + for listener in modifiers_changed_listeners { + window.on_modifiers_changed(move |event: &ModifiersChangedEvent, window, cx| { + listener(event, window, cx); + }) + } + + for (action_type, listener) in action_listeners { + window.on_action(action_type, listener) + } + } + + fn paint_hover_group_handler(&self, window: &mut Window, cx: &mut App) { + let group_hitbox = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupHitboxes::get(&group_hover.group, cx)); + + if let Some(group_hitbox) = group_hitbox { + let was_hovered = group_hitbox.is_hovered(window); + let current_view = window.current_view(); + window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| { + let hovered = group_hitbox.is_hovered(window); + if phase == DispatchPhase::Capture && hovered != was_hovered { + cx.notify(current_view); + } + }); + } + } + + fn paint_scroll_listener( + &self, + hitbox: &Hitbox, + style: &Style, + window: &mut Window, + _cx: &mut App, + ) { + if let Some(scroll_offset) = self.scroll_offset.clone() { + let ongoing_scroll = self.ongoing_scroll.clone(); + let overflow = style.overflow; + let allow_concurrent_scroll = style.allow_concurrent_scroll; + let restrict_scroll_to_axis = style.restrict_scroll_to_axis; + let line_height = window.line_height(); + let hitbox = hitbox.clone(); + let current_view = window.current_view(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + let mut scroll_offset = scroll_offset.borrow_mut(); + let old_scroll_offset = *scroll_offset; + let mut delta = event.delta.pixel_delta(line_height); + + if restrict_scroll_to_axis + && event.delta.precise() + && let Some(ongoing_scroll) = &ongoing_scroll + { + ongoing_scroll + .borrow_mut() + .filter(&mut delta, event.touch_phase); + } + + let mut delta_x = match overflow.x { + Overflow::Scroll if !delta.x.is_zero() => delta.x, + Overflow::Scroll + if !restrict_scroll_to_axis && overflow.y != Overflow::Scroll => + { + delta.y + } + _ => Pixels::ZERO, + }; + let mut delta_y = match overflow.y { + Overflow::Scroll if !delta.y.is_zero() => delta.y, + Overflow::Scroll + if !restrict_scroll_to_axis && overflow.x != Overflow::Scroll => + { + delta.x + } + _ => Pixels::ZERO, + }; + if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() { + if delta_x.abs() > delta_y.abs() { + delta_y = Pixels::ZERO; + } else { + delta_x = Pixels::ZERO; + } + } + scroll_offset.y += delta_y; + scroll_offset.x += delta_x; + if *scroll_offset != old_scroll_offset { + cx.notify(current_view); + } + } + }); + } + } + + /// Compute the visual style for this element, based on the current bounds and the element's state. + pub fn compute_style( + &self, + global_id: Option<&GlobalElementId>, + hitbox: Option<&Hitbox>, + window: &mut Window, + cx: &mut App, + ) -> Style { + window.with_optional_element_state(global_id, |element_state, window| { + let mut element_state = + element_state.map(|element_state| element_state.unwrap_or_default()); + let style = self.compute_style_internal(hitbox, element_state.as_mut(), window, cx); + (style, element_state) + }) + } + + /// Called from internal methods that have already called with_element_state. + fn compute_style_internal( + &self, + hitbox: Option<&Hitbox>, + element_state: Option<&mut InteractiveElementState>, + window: &mut Window, + cx: &mut App, + ) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() + && focus_handle.within_focused(window, cx) + { + style.refine(in_focus_style); + } + + if let Some(focus_style) = self.focus_style.as_ref() + && focus_handle.is_focused(window) + { + style.refine(focus_style); + } + + if let Some(focus_visible_style) = self.focus_visible_style.as_ref() + && focus_handle.is_focused(window) + && window.last_input_was_keyboard() + { + style.refine(focus_visible_style); + } + } + + if !cx.has_active_drag() { + if let Some(group_hover) = self.group_hover_style.as_ref() { + let is_group_hovered = + if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) { + !window.last_input_was_touch() && group_hitbox_id.is_hovered(window) + } else if let Some(element_state) = element_state.as_ref() { + !window.last_input_was_touch() + && element_state + .hover_state + .as_ref() + .map(|state| state.borrow().group) + .unwrap_or(false) + } else { + false + }; + + if is_group_hovered { + style.refine(&group_hover.style); + } + } + + if let Some(hover_style) = self.hover_style.as_ref() { + let is_hovered = if let Some(hitbox) = hitbox { + !window.last_input_was_touch() && hitbox.is_hovered(window) + } else if let Some(element_state) = element_state.as_ref() { + !window.last_input_was_touch() + && element_state + .hover_state + .as_ref() + .map(|state| state.borrow().element) + .unwrap_or(false) + } else { + false + }; + + if is_hovered { + style.refine(hover_style); + } + } + } + + if let Some(hitbox) = hitbox { + if let Some(drag) = cx.active_drag.take() { + let mut can_drop = true; + if let Some(can_drop_predicate) = &self.can_drop_predicate { + can_drop = can_drop_predicate(drag.value.as_ref(), window, cx); + } + + if can_drop { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_hitbox_id) = + GroupHitboxes::get(&group_drag_style.group, cx) + && *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_drag_style.style); + } + } + + for (state_type, build_drag_over_style) in &self.drag_over_styles { + if *state_type == drag.value.as_ref().type_id() && hitbox.is_hovered(window) + { + style.refine(&build_drag_over_style(drag.value.as_ref(), window, cx)); + } + } + } + + style.mouse_cursor = drag.cursor_style; + cx.active_drag = Some(drag); + } + } + + if let Some(element_state) = element_state { + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .borrow(); + if clicked_state.group + && let Some(group) = self.group_active_style.as_ref() + { + style.refine(&group.style) + } + + if let Some(active_style) = self.active_style.as_ref() + && clicked_state.element + { + style.refine(active_style) + } + } + + style + } + + pub(crate) fn write_a11y_info(&self, node: &mut accesskit::Node) { + if let Some(id) = &self.aria.author_id { + node.set_author_id(id.to_string()); + } + if let Some(label) = &self.aria.label { + node.set_label(label.to_string()); + } + if let Some(description) = &self.aria.description { + node.set_description(description.to_string()); + } + if let Some(keyshortcuts) = &self.aria.keyshortcuts { + node.set_keyboard_shortcut(keyshortcuts.to_string()); + } + if let Some(selected) = self.aria.selected { + node.set_selected(selected); + } + if let Some(expanded) = self.aria.expanded { + node.set_expanded(expanded); + } + if let Some(toggled) = self.aria.toggled { + node.set_toggled(toggled); + } + if let Some(value) = self.aria.numeric_value { + node.set_numeric_value(value); + } + if let Some(value) = self.aria.min_numeric_value { + node.set_min_numeric_value(value); + } + if let Some(value) = self.aria.max_numeric_value { + node.set_max_numeric_value(value); + } + if let Some(step) = self.aria.numeric_value_step { + node.set_numeric_value_step(step); + } + if let Some(value) = &self.aria.value { + node.set_value(value.to_string()); + } + if let Some(placeholder) = &self.aria.placeholder { + node.set_placeholder(placeholder.to_string()); + } + if let Some(orientation) = self.aria.orientation { + node.set_orientation(orientation); + } + if let Some(level) = self.aria.level { + node.set_level(level); + } + if let Some(position) = self.aria.position_in_set { + node.set_position_in_set(position); + } + if let Some(size) = self.aria.size_of_set { + node.set_size_of_set(size); + } + if let Some(index) = self.aria.row_index { + node.set_row_index(index); + } + if let Some(index) = self.aria.column_index { + node.set_column_index(index); + } + if let Some(count) = self.aria.row_count { + node.set_row_count(count); + } + if let Some(count) = self.aria.column_count { + node.set_column_count(count); + } + if !self.click_listeners.is_empty() { + node.add_action(accesskit::Action::Click); + } + if self.tracked_focus_handle.is_some() || self.focusable { + node.add_action(accesskit::Action::Focus); + } + for (action, _) in &self.a11y_action_listeners { + node.add_action(*action); + } + } +} + +/// The per-frame state of an interactive element. Used for tracking stateful interactions like clicks +/// and scroll offsets. +#[derive(Default)] +pub struct InteractiveElementState { + pub(crate) focus_handle: Option, + pub(crate) clicked_state: Option>>, + pub(crate) hover_state: Option>>, + pub(crate) hover_listener_state: Option>>, + pub(crate) pending_mouse_down: Option>>>, + /// Set to the window's [`focus_generation`](crate::Window::focus_generation) + /// when an Enter/Space keydown is received while this element is focused, + /// recording that we are waiting for the matching keyup to fire a keyboard + /// click. On keyup the click only fires if the stored generation still + /// matches the window's current one, i.e. focus never moved during the + /// press (mirroring the browser clearing a control's pressed state on + /// blur). `None` means no activation key is pending. + pub(crate) pending_keyboard_down: Option>>>, + pub(crate) scroll_offset: Option>>>, + ongoing_scroll: Option>>, + pub(crate) active_tooltip: Option>>>, + long_press_tooltip_active: Option>>, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub struct ElementClickedState { + /// True if this element's group has been clicked, false otherwise + pub group: bool, + + /// True if this element has been clicked, false otherwise + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + +/// Whether or not the element or a group that contains it is hovered. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub struct ElementHoverState { + /// True if this element's group is hovered, false otherwise + pub group: bool, + + /// True if this element is hovered, false otherwise + pub element: bool, +} + +pub(crate) enum ActiveTooltip { + /// Currently delaying before showing the tooltip. + WaitingForShow { _task: Task<()> }, + /// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered. + Visible { + tooltip: AnyTooltip, + is_hoverable: bool, + }, + /// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying + /// before hiding it. + WaitingForHide { + tooltip: AnyTooltip, + _task: Task<()>, + }, +} + +pub(crate) fn clear_active_tooltip( + active_tooltip: &Rc>>, + window: &mut Window, +) { + match active_tooltip.borrow_mut().take() { + None => {} + Some(ActiveTooltip::WaitingForShow { .. }) => {} + Some(ActiveTooltip::Visible { .. }) => window.refresh(), + Some(ActiveTooltip::WaitingForHide { .. }) => window.refresh(), + } +} + +pub(crate) fn clear_active_tooltip_if_not_hoverable( + active_tooltip: &Rc>>, + window: &mut Window, +) { + let should_clear = match active_tooltip.borrow().as_ref() { + None => false, + Some(ActiveTooltip::WaitingForShow { .. }) => false, + Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable, + Some(ActiveTooltip::WaitingForHide { .. }) => false, + }; + if should_clear { + active_tooltip.borrow_mut().take(); + window.refresh(); + } +} + +pub(crate) fn set_tooltip_on_window( + active_tooltip: &Rc>>, + window: &mut Window, +) -> Option { + let tooltip = match active_tooltip.borrow().as_ref() { + None => return None, + Some(ActiveTooltip::WaitingForShow { .. }) => return None, + Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(), + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(), + }; + Some(window.set_tooltip(tooltip)) +} + +pub(crate) fn register_tooltip_mouse_handlers( + active_tooltip: &Rc>>, + tooltip_id: Option, + build_tooltip: Rc Option<(AnyView, bool)>>, + check_is_hovered: Rc bool>, + check_is_hovered_during_prepaint: Rc bool>, + long_press_tooltip_active: Rc>, + show_delay: Option, + window: &mut Window, +) { + let current_view = window.current_view(); + let show_delay = show_delay.unwrap_or(DEFAULT_TOOLTIP_SHOW_DELAY); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered = check_is_hovered.clone(); + let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + handle_tooltip_mouse_move( + &active_tooltip, + &build_tooltip, + &check_is_hovered, + &check_is_hovered_during_prepaint, + tooltip_id, + current_view, + phase, + show_delay, + window, + cx, + ) + } + }); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, window); + } + } + }); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); + move |event: &LongPressEvent, phase, window, cx| { + if !phase.bubble() { + return; + } + + match event.phase { + TouchPhase::Started if !window.default_prevented() && check_is_hovered(window) => { + if show_tooltip( + &active_tooltip, + &build_tooltip, + &check_is_hovered_during_prepaint, + Some(long_press_tooltip_active.clone()), + window, + cx, + ) { + long_press_tooltip_active.set(true); + window.prevent_default(); + cx.stop_propagation(); + } + } + TouchPhase::Ended | TouchPhase::Cancelled + if long_press_tooltip_active.replace(false) => + { + clear_active_tooltip(&active_tooltip, window); + cx.stop_propagation(); + } + TouchPhase::Started + | TouchPhase::Moved + | TouchPhase::Ended + | TouchPhase::Cancelled => {} + } + } + }); + + window.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| { + if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, window); + } + } + }); +} + +/// Handles displaying tooltips when an element is hovered. +/// +/// The mouse hovering logic also relies on being called from window prepaint in order to handle the +/// case where the element the tooltip is on is not rendered - in that case its mouse listeners are +/// also not registered. During window prepaint, the hitbox information is not available, so +/// `check_is_hovered_during_prepaint` is used which bases the check off of the absolute bounds of +/// the element. +/// +/// TODO: There's a minor bug due to the use of absolute bounds while checking during prepaint - it +/// does not know if the hitbox is occluded. In the case where a tooltip gets displayed and then +/// gets occluded after display, it will stick around until the mouse exits the hover bounds. +fn handle_tooltip_mouse_move( + active_tooltip: &Rc>>, + build_tooltip: &Rc Option<(AnyView, bool)>>, + check_is_hovered: &Rc bool>, + check_is_hovered_during_prepaint: &Rc bool>, + tooltip_id: Option, + current_view: EntityId, + phase: DispatchPhase, + show_delay: Duration, + window: &mut Window, + cx: &mut App, +) { + // Separates logic for what mutation should occur from applying it, to avoid overlapping + // RefCell borrows. + enum Action { + None, + CancelShow, + ScheduleShow, + CheckVisible, + } + + let action = match active_tooltip.borrow().as_ref() { + None => { + let is_hovered = check_is_hovered(window); + if is_hovered && phase.bubble() { + Action::ScheduleShow + } else { + Action::None + } + } + Some(ActiveTooltip::WaitingForShow { .. }) => { + let is_hovered = check_is_hovered(window); + if is_hovered { + Action::None + } else { + Action::CancelShow + } + } + Some(ActiveTooltip::Visible { is_hoverable, .. }) => { + if phase.capture() + && !check_is_hovered(window) + && (!*is_hoverable + || !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window))) + { + Action::CheckVisible + } else { + Action::None + } + } + Some(ActiveTooltip::WaitingForHide { .. }) => { + if phase.capture() + && (check_is_hovered(window) + || tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window))) + { + Action::CheckVisible + } else { + Action::None + } + } + }; + + match action { + Action::None => {} + Action::CancelShow => { + // Cancel waiting to show tooltip when it is no longer hovered. + active_tooltip.borrow_mut().take(); + } + Action::ScheduleShow => { + let delayed_show_task = window.spawn(cx, { + let weak_active_tooltip = Rc::downgrade(active_tooltip); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); + async move |cx| { + cx.background_executor().timer(show_delay).await; + let Some(active_tooltip) = weak_active_tooltip.upgrade() else { + return; + }; + cx.update(|window, cx| { + show_tooltip( + &active_tooltip, + &build_tooltip, + &check_is_hovered_during_prepaint, + None, + window, + cx, + ); + }) + .ok(); + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForShow { + _task: delayed_show_task, + }); + } + Action::CheckVisible => cx.notify(current_view), + } +} + +fn show_tooltip( + active_tooltip: &Rc>>, + build_tooltip: &Rc Option<(AnyView, bool)>>, + check_is_hovered_during_prepaint: &Rc bool>, + long_press_tooltip_active: Option>>, + window: &mut Window, + cx: &mut App, +) -> bool { + let new_tooltip = build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| { + let weak_active_tooltip = Rc::downgrade(active_tooltip); + let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); + ActiveTooltip::Visible { + tooltip: AnyTooltip { + view, + mouse_position: window.mouse_position(), + check_visible_and_update: Rc::new(move |tooltip_bounds, window, cx| { + if long_press_tooltip_active + .as_ref() + .is_some_and(|active| active.get()) + { + return true; + } + let Some(active_tooltip) = weak_active_tooltip.upgrade() else { + return false; + }; + handle_tooltip_check_visible_and_update( + &active_tooltip, + tooltip_is_hoverable, + &check_is_hovered_during_prepaint, + tooltip_bounds, + window, + cx, + ) + }), + }, + is_hoverable: tooltip_is_hoverable, + } + }); + let did_show = new_tooltip.is_some(); + *active_tooltip.borrow_mut() = new_tooltip; + window.refresh(); + did_show +} + +/// Returns a callback which will be called by window prepaint to update tooltip visibility. The +/// purpose of doing this logic here instead of the mouse move handler is that the mouse move +/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`). +fn handle_tooltip_check_visible_and_update( + active_tooltip: &Rc>>, + tooltip_is_hoverable: bool, + check_is_hovered: &Rc bool>, + tooltip_bounds: Bounds, + window: &mut Window, + cx: &mut App, +) -> bool { + // Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell + // borrows. + enum Action { + None, + Hide, + ScheduleHide(AnyTooltip), + CancelHide(AnyTooltip), + } + + let is_hovered = check_is_hovered(window) + || (tooltip_is_hoverable && tooltip_bounds.contains(&window.mouse_position())); + let action = match active_tooltip.borrow().as_ref() { + Some(ActiveTooltip::Visible { tooltip, .. }) => { + if is_hovered { + Action::None + } else { + if tooltip_is_hoverable { + Action::ScheduleHide(tooltip.clone()) + } else { + Action::Hide + } + } + } + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => { + if is_hovered { + Action::CancelHide(tooltip.clone()) + } else { + Action::None + } + } + None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None, + }; + + match action { + Action::None => {} + Action::Hide => clear_active_tooltip(active_tooltip, window), + Action::ScheduleHide(tooltip) => { + let delayed_hide_task = window.spawn(cx, { + let weak_active_tooltip = Rc::downgrade(active_tooltip); + async move |cx| { + cx.background_executor() + .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) + .await; + let Some(active_tooltip) = weak_active_tooltip.upgrade() else { + return; + }; + if active_tooltip.borrow_mut().take().is_some() { + cx.update(|window, _cx| window.refresh()).ok(); + } + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForHide { + tooltip, + _task: delayed_hide_task, + }); + } + Action::CancelHide(tooltip) => { + // Cancel waiting to hide tooltip when it becomes hovered. + active_tooltip.borrow_mut().replace(ActiveTooltip::Visible { + tooltip, + is_hoverable: true, + }); + } + } + + active_tooltip.borrow().is_some() +} + +#[derive(Default)] +pub(crate) struct GroupHitboxes(HashMap>); + +impl Global for GroupHitboxes {} + +impl GroupHitboxes { + pub fn get(name: &SharedString, cx: &mut App) -> Option { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, hitbox_id: HitboxId, cx: &mut App) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(hitbox_id); + } + + pub fn pop(name: &SharedString, cx: &mut App) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +/// A wrapper around an element that can store state, produced after assigning an ElementId. +pub struct Stateful { + pub(crate) element: E, +} + +impl Styled for Stateful +where + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + +impl StatefulInteractiveElement for Stateful +where + E: Element, + Self: InteractiveElement, +{ +} + +impl InteractiveElement for Stateful +where + E: InteractiveElement, +{ + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl Element for Stateful +where + E: Element, +{ + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; + + fn id(&self) -> Option { + self.element.id() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + self.element.source_location() + } + + fn a11y_role(&self) -> Option { + self.element.a11y_role() + } + + fn write_a11y_info(&self, node: &mut accesskit::Node) { + self.element.write_a11y_info(node); + } + + fn a11y_synthetic_children( + &mut self, + prepaint: &mut Self::PrepaintState, + builder: &mut crate::A11ySubtreeBuilder, + ) { + self.element.a11y_synthetic_children(prepaint, builder); + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(id, inspector_id, window, cx) + } + + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> E::PrepaintState { + self.element + .prepaint(id, inspector_id, bounds, state, window, cx) + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.element.paint( + id, + inspector_id, + bounds, + request_layout, + prepaint, + window, + cx, + ); + } +} + +impl IntoElement for Stateful +where + E: Element, +{ + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl ParentElement for Stateful +where + E: ParentElement, +{ + fn extend(&mut self, elements: impl IntoIterator) { + self.element.extend(elements) + } +} + +/// Represents an element that can be scrolled *to* in its parent element. +/// Contrary to [ScrollHandle::scroll_to_active_item], an anchored element does not have to be an immediate child of the parent. +#[derive(Clone)] +pub struct ScrollAnchor { + handle: ScrollHandle, + last_origin: Rc>>, +} + +impl ScrollAnchor { + /// Creates a [ScrollAnchor] associated with a given [ScrollHandle]. + pub fn for_handle(handle: ScrollHandle) -> Self { + Self { + handle, + last_origin: Default::default(), + } + } + /// Request scroll to this item on the next frame. + pub fn scroll_to(&self, window: &mut Window, _cx: &mut App) { + let this = self.clone(); + + window.on_next_frame(move |_, _| { + let viewport_bounds = this.handle.bounds(); + let self_bounds = *this.last_origin.borrow(); + this.handle.set_offset(viewport_bounds.origin - self_bounds); + }); + } +} + +#[derive(Default, Debug)] +struct ScrollHandleState { + offset: Rc>>, + ongoing_scroll: Rc>, + bounds: Bounds, + max_offset: Point, + child_bounds: Vec>, + scroll_to_bottom: bool, + overflow: Point, + active_item: Option, +} + +#[derive(Default, Debug, Clone, Copy)] +struct ScrollActiveItem { + index: usize, + strategy: ScrollStrategy, +} + +#[derive(Default, Debug, Clone, Copy)] +enum ScrollStrategy { + #[default] + FirstVisible, + Top, +} + +/// A handle to the scrollable aspects of an element. +/// Used for accessing scroll state, like the current scroll offset, +/// and for mutating the scroll state, like scrolling to a specific child. +#[derive(Clone, Debug)] +pub struct ScrollHandle(Rc>); + +impl Default for ScrollHandle { + fn default() -> Self { + Self::new() + } +} + +impl ScrollHandle { + /// Construct a new scroll handle. + pub fn new() -> Self { + Self(Rc::default()) + } + + /// Get the current scroll offset. + pub fn offset(&self) -> Point { + *self.0.borrow().offset.borrow() + } + + /// Get the maximum scroll offset. + pub fn max_offset(&self) -> Point { + self.0.borrow().max_offset + } + + /// Get the top child that's scrolled into view. + pub fn top_item(&self) -> usize { + let state = self.0.borrow(); + let top = state.bounds.top() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if top < bounds.top() { + Ordering::Greater + } else if top > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + /// Get the bottom child that's scrolled into view. + pub fn bottom_item(&self) -> usize { + let state = self.0.borrow(); + let bottom = state.bounds.bottom() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if bottom < bounds.top() { + Ordering::Greater + } else if bottom > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + + /// Return the bounds into which this child is painted + pub fn bounds(&self) -> Bounds { + self.0.borrow().bounds + } + + /// Get the bounds for a specific child. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + self.0.borrow().child_bounds.get(ix).cloned() + } + + /// Update [ScrollHandleState]'s active item for scrolling to in prepaint + pub fn scroll_to_item(&self, ix: usize) { + let mut state = self.0.borrow_mut(); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::default(), + }); + } + + /// Update [ScrollHandleState]'s active item for scrolling to in prepaint + /// This scrolls the minimal amount to ensure that the child is the first visible element + pub fn scroll_to_top_of_item(&self, ix: usize) { + let mut state = self.0.borrow_mut(); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::Top, + }); + } + + /// Scrolls the minimal amount to either ensure that the child is + /// fully visible or the top element of the view depends on the + /// scroll strategy + fn scroll_to_active_item(&self) { + let mut state = self.0.borrow_mut(); + + let Some(active_item) = state.active_item else { + return; + }; + + let active_item = match state.child_bounds.get(active_item.index) { + Some(bounds) => { + let mut scroll_offset = state.offset.borrow_mut(); + + match active_item.strategy { + ScrollStrategy::FirstVisible => { + if state.overflow.y == Overflow::Scroll { + let child_height = bounds.size.height; + let viewport_height = state.bounds.size.height; + if child_height > viewport_height { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.top() + scroll_offset.y < state.bounds.top() { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { + scroll_offset.y = state.bounds.bottom() - bounds.bottom(); + } + } + } + ScrollStrategy::Top => { + scroll_offset.y = state.bounds.top() - bounds.top(); + } + } + + if state.overflow.x == Overflow::Scroll { + let child_width = bounds.size.width; + let viewport_width = state.bounds.size.width; + if child_width > viewport_width { + scroll_offset.x = state.bounds.left() - bounds.left(); + } else if bounds.left() + scroll_offset.x < state.bounds.left() { + scroll_offset.x = state.bounds.left() - bounds.left(); + } else if bounds.right() + scroll_offset.x > state.bounds.right() { + scroll_offset.x = state.bounds.right() - bounds.right(); + } + } + None + } + None => Some(active_item), + }; + state.active_item = active_item; + } + + /// Scrolls to the bottom. + pub fn scroll_to_bottom(&self) { + let mut state = self.0.borrow_mut(); + state.scroll_to_bottom = true; + } + + /// Set the offset explicitly. The offset is the distance from the top left of the + /// parent container to the top left of the first child. + /// As you scroll further down the offset becomes more negative. + pub fn set_offset(&self, mut position: Point) { + let state = self.0.borrow(); + *state.offset.borrow_mut() = position; + } + + /// Get the logical scroll top, based on a child index and a pixel offset. + pub fn logical_scroll_top(&self) -> (usize, Pixels) { + let ix = self.top_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.top() + state.offset.borrow().y - state.bounds.top(), + ) + } else { + (ix, px(0.)) + } + } + + /// Get the logical scroll bottom, based on a child index and a pixel offset. + pub fn logical_scroll_bottom(&self) -> (usize, Pixels) { + let ix = self.bottom_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.bottom() + state.offset.borrow().y - state.bounds.bottom(), + ) + } else { + (ix, px(0.)) + } + } + + /// Get the count of children for scrollable item. + pub fn children_count(&self) -> usize { + self.0.borrow().child_bounds.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AnyWindowHandle, AppContext as _, Context, GestureTuning, InputEvent, Keystroke, + MouseMoveEvent, TestAppContext, TouchEvent, TouchId, canvas, util::FluentBuilder as _, + }; + use std::{cell::Cell, rc::Weak}; + + struct GroupHoverTestView { + render_count: Rc>, + anonymous_paint_count: Rc>, + stateful_width: Rc>, + } + + impl Render for GroupHoverTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.render_count.set(self.render_count.get() + 1); + let anonymous_paint_count = self.anonymous_paint_count.clone(); + let stateful_width = self.stateful_width.clone(); + div().size_full().child( + div() + .ml(px(20.)) + .mt(px(20.)) + .size(px(50.)) + .relative() + .group("hover-group") + .child( + div() + .absolute() + .size_full() + .invisible() + .group_hover("hover-group", |style| style.visible()) + .child(canvas( + |_, _, _| {}, + move |_, _, _, _| { + anonymous_paint_count.set(anonymous_paint_count.get() + 1) + }, + )), + ) + .child( + div() + .id("stateful-group-hover-target") + .absolute() + .top_0() + .left_0() + .size(px(10.)) + .group_hover("hover-group", |style| style.size(px(20.))) + .child(canvas( + move |bounds, _, _| stateful_width.set(bounds.size.width), + |_, _, _, _| {}, + )), + ), + ) + } + } + + #[gpui::test] + fn group_hover_styles_update_only_on_transitions(cx: &mut TestAppContext) { + let render_count = Rc::new(Cell::new(0)); + let anonymous_paint_count = Rc::new(Cell::new(0)); + let stateful_width = Rc::new(Cell::new(px(0.))); + let window = cx.add_window({ + let render_count = render_count.clone(); + let anonymous_paint_count = anonymous_paint_count.clone(); + let stateful_width = stateful_width.clone(); + move |_, _| GroupHoverTestView { + render_count, + anonymous_paint_count, + stateful_width, + } + }); + let window = AnyWindowHandle::from(window); + + cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(anonymous_paint_count.get(), 0); + assert_eq!(stateful_width.get(), px(10.)); + + let move_mouse = |cx: &mut TestAppContext, position| { + cx.update_window(window, |_, window, cx| { + window.simulate_mouse_move(position, cx) + }) + .unwrap(); + }; + + let initial_render_count = render_count.get(); + move_mouse(cx, point(px(25.), px(25.))); + assert_eq!(render_count.get(), initial_render_count + 1); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(20.)); + + move_mouse(cx, point(px(30.), px(30.))); + assert_eq!(render_count.get(), initial_render_count + 1); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(20.)); + + move_mouse(cx, point(px(5.), px(5.))); + assert_eq!(render_count.get(), initial_render_count + 2); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(10.)); + + move_mouse(cx, point(px(10.), px(10.))); + assert_eq!(render_count.get(), initial_render_count + 2); + assert_eq!(anonymous_paint_count.get(), 1); + assert_eq!(stateful_width.get(), px(10.)); + } + + struct HoverListenerLayoutTestView { + target_left: Pixels, + hover_transitions: Rc>>, + } + + impl Render for HoverListenerLayoutTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let hover_transitions = self.hover_transitions.clone(); + div().relative().size_full().child( + div() + .id("hover-target") + .absolute() + .left(self.target_left) + .top_0() + .size(px(20.)) + .on_click(|_, _, _| {}) + .on_hover(move |is_hovered, _, _| { + hover_transitions.borrow_mut().push(*is_hovered); + }), + ) + } + } + + #[gpui::test] + fn default_hover_listener_updates_when_layout_changes_under_stationary_mouse( + cx: &mut TestAppContext, + ) { + let hover_transitions = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let hover_transitions = hover_transitions.clone(); + move |_, _| HoverListenerLayoutTestView { + target_left: px(40.), + hover_transitions, + } + }); + let any_window = AnyWindowHandle::from(window); + + cx.update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + window.simulate_mouse_move(point(px(10.), px(10.)), cx); + }) + .unwrap(); + assert!(hover_transitions.borrow().is_empty()); + + window + .update(cx, |view, _, cx| { + view.target_left = px(0.); + cx.notify(); + }) + .unwrap(); + cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + window + .update(cx, |view, _, cx| { + view.target_left = px(40.); + cx.notify(); + }) + .unwrap(); + cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true, false]); + } + + #[gpui::test] + fn default_hover_listener_ends_after_key_press(cx: &mut TestAppContext) { + let hover_transitions = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let hover_transitions = hover_transitions.clone(); + move |_, _| HoverListenerLayoutTestView { + target_left: px(0.), + hover_transitions, + } + }); + let any_window = AnyWindowHandle::from(window); + + cx.update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + window.simulate_mouse_move(point(px(10.), px(10.)), cx); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + key_down(cx, any_window, "a"); + cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true, false]); + } + + struct HoverListenerModeLayoutTestView { + target_left: Pixels, + hover_transitions: Rc>>, + } + + impl Render for HoverListenerModeLayoutTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let hover_transitions = self.hover_transitions.clone(); + div().relative().size_full().child( + div() + .id("hover-target") + .absolute() + .left(self.target_left) + .top_0() + .size(px(20.)) + .hover_listener_mode(HoverListenerMode::InputModalityIndependent) + .on_hover(move |is_hovered, _, _| { + hover_transitions.borrow_mut().push(*is_hovered); + }), + ) + } + } + + #[gpui::test] + fn input_modality_independent_hover_listener_updates_after_key_press(cx: &mut TestAppContext) { + let hover_transitions = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let hover_transitions = hover_transitions.clone(); + move |_, _| HoverListenerModeLayoutTestView { + target_left: px(40.), + hover_transitions, + } + }); + let any_window = AnyWindowHandle::from(window); + let pointer_position = point(px(10.), px(10.)); + + cx.update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + window.simulate_mouse_move(pointer_position, cx); + }) + .unwrap(); + assert!(hover_transitions.borrow().is_empty()); + + key_down(cx, any_window, "a"); + window + .update(cx, |view, _, cx| { + view.target_left = px(0.); + cx.notify(); + }) + .unwrap(); + cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + key_down(cx, any_window, "b"); + cx.update_window(any_window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + cx.update_window(any_window, |_, window, cx| { + window.simulate_mouse_move(point(px(30.), px(10.)), cx); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true, false]); + + cx.update_window(any_window, |_, window, cx| { + window.simulate_mouse_move(pointer_position, cx); + window.dispatch_event( + MouseExitEvent { + position: pointer_position, + ..Default::default() + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true, false, true, false]); + } + + #[gpui::test] + fn default_hover_listener_remains_hovered_during_stationary_mouse_press( + cx: &mut TestAppContext, + ) { + let hover_transitions = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let hover_transitions = hover_transitions.clone(); + move |_, _| HoverListenerLayoutTestView { + target_left: px(0.), + hover_transitions, + } + }); + let any_window = AnyWindowHandle::from(window); + let mouse_position = point(px(10.), px(10.)); + + cx.update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + window.simulate_mouse_move(mouse_position, cx); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + cx.update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseDownEvent { + position: mouse_position, + button: MouseButton::Left, + modifiers: Default::default(), + click_count: 1, + first_mouse: false, + } + .to_platform_input(), + cx, + ); + window.draw(cx).clear(cx); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + + cx.update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseUpEvent { + position: mouse_position, + button: MouseButton::Left, + modifiers: Default::default(), + click_count: 1, + } + .to_platform_input(), + cx, + ); + window.draw(cx).clear(cx); + }) + .unwrap(); + assert_eq!(*hover_transitions.borrow(), [true]); + } + + struct TestTooltipView; + + impl Render for TestTooltipView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(20.)).h(px(20.)).child("tooltip") + } + } + + type CapturedActiveTooltip = Rc>>>>>; + + struct TooltipCaptureElement { + child: AnyElement, + captured_active_tooltip: CapturedActiveTooltip, + } + + impl IntoElement for TooltipCaptureElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl Element for TooltipCaptureElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (self.child.request_layout(window, cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + self.child.prepaint(window, cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.child.paint(window, cx); + window.with_global_id("target".into(), |global_id, window| { + window.with_element_state::( + global_id, + |state, _window| { + let state = state.unwrap(); + *self.captured_active_tooltip.borrow_mut() = + state.active_tooltip.as_ref().map(Rc::downgrade); + ((), state) + }, + ) + }); + } + } + + struct TooltipOwner { + captured_active_tooltip: CapturedActiveTooltip, + show_delay_override: Option, + } + + impl Render for TooltipOwner { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + TooltipCaptureElement { + child: div() + .size_full() + .child( + div() + .id("target") + .w(px(50.)) + .h(px(50.)) + .tooltip(|_, cx| cx.new(|_| TestTooltipView).into()) + .when_some(self.show_delay_override, |this, delay| { + this.tooltip_show_delay(delay) + }), + ) + .into_any_element(), + captured_active_tooltip: self.captured_active_tooltip.clone(), + } + } + } + + #[test] + fn scroll_handle_aligns_wide_children_to_left_edge() { + let handle = ScrollHandle::new(); + { + let mut state = handle.0.borrow_mut(); + state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(80.), px(20.))); + state.child_bounds = vec![Bounds::new(point(px(25.), px(0.)), size(px(200.), px(20.)))]; + state.overflow.x = Overflow::Scroll; + state.active_item = Some(ScrollActiveItem { + index: 0, + strategy: ScrollStrategy::default(), + }); + } + + handle.scroll_to_active_item(); + + assert_eq!(handle.offset().x, px(-25.)); + } + + #[test] + fn scroll_handle_aligns_tall_children_to_top_edge() { + let handle = ScrollHandle::new(); + { + let mut state = handle.0.borrow_mut(); + state.bounds = Bounds::new(point(px(0.), px(0.)), size(px(20.), px(80.))); + state.child_bounds = vec![Bounds::new(point(px(0.), px(25.)), size(px(20.), px(200.)))]; + state.overflow.y = Overflow::Scroll; + state.active_item = Some(ScrollActiveItem { + index: 0, + strategy: ScrollStrategy::default(), + }); + } + + handle.scroll_to_active_item(); + + assert_eq!(handle.offset().y, px(-25.)); + } + + #[test] + fn long_press_shows_tooltip_until_touch_ends() { + let (mut test_app, any_window, captured_active_tooltip) = create_tooltip_owner_test(None); + let active_tooltip = captured_active_tooltip + .borrow() + .clone() + .unwrap() + .upgrade() + .unwrap(); + let touch_id = TouchId(1); + let touch_position = point(px(49.), px(10.)); + + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + TouchEvent { + id: touch_id, + phase: TouchPhase::Started, + position: touch_position, + predicted_position: None, + force: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + test_app + .dispatcher + .advance_clock(GestureTuning::default().long_press_duration + Duration::from_millis(1)); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + + let moved_position = point(px(55.), px(10.)); + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + TouchEvent { + id: touch_id, + phase: TouchPhase::Moved, + position: moved_position, + predicted_position: None, + force: None, + } + .to_platform_input(), + cx, + ); + window.draw(cx).clear(cx); + }) + .unwrap(); + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + TouchEvent { + id: touch_id, + phase: TouchPhase::Ended, + position: moved_position, + predicted_position: None, + force: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + + assert!(active_tooltip.borrow().is_none()); + } + + fn setup_tooltip_owner_test( + show_delay_override: Option, + ) -> ( + TestAppContext, + crate::AnyWindowHandle, + CapturedActiveTooltip, + ) { + let (mut test_app, any_window, captured_active_tooltip) = + create_tooltip_owner_test(show_delay_override); + + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: point(px(10.), px(10.)), + modifiers: Default::default(), + pressed_button: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + + (test_app, any_window, captured_active_tooltip) + } + + fn create_tooltip_owner_test( + show_delay_override: Option, + ) -> ( + TestAppContext, + crate::AnyWindowHandle, + CapturedActiveTooltip, + ) { + let mut test_app = TestAppContext::single(); + let captured_active_tooltip: CapturedActiveTooltip = Rc::new(RefCell::new(None)); + let window = test_app.add_window({ + let captured_active_tooltip = captured_active_tooltip.clone(); + move |_, _| TooltipOwner { + captured_active_tooltip, + show_delay_override, + } + }); + let any_window = window.into(); + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + + (test_app, any_window, captured_active_tooltip) + } + + #[test] + fn tooltip_waiting_for_show_is_released_when_its_owner_disappears() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::WaitingForShow { .. }) + )); + + test_app + .update_window(any_window, |_, window, _| { + window.remove_window(); + }) + .unwrap(); + test_app.run_until_parked(); + drop(active_tooltip); + + assert!(weak_active_tooltip.upgrade().is_none()); + } + + #[test] + fn tooltip_respects_custom_show_delay() { + let extra_delay = Duration::from_secs(1); + let show_delay_override = DEFAULT_TOOLTIP_SHOW_DELAY + extra_delay; + let (mut test_app, _any_window, captured_active_tooltip) = + setup_tooltip_owner_test(Some(show_delay_override)); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + + test_app + .dispatcher + .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::WaitingForShow { .. }) + )); + + test_app.dispatcher.advance_clock(extra_delay); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + } + + #[test] + fn tooltip_is_released_when_its_owner_disappears() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + + test_app + .dispatcher + .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + + test_app + .update_window(any_window, |_, window, _| { + window.remove_window(); + }) + .unwrap(); + test_app.run_until_parked(); + drop(active_tooltip); + + assert!(weak_active_tooltip.upgrade().is_none()); + } + + #[test] + fn tooltip_hides_after_mouse_leaves_origin() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(None); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + + test_app + .dispatcher + .advance_clock(DEFAULT_TOOLTIP_SHOW_DELAY); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: point(px(75.), px(75.)), + modifiers: Default::default(), + pressed_button: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + + assert!(active_tooltip.borrow().is_none()); + } + + struct MouseDownOutOwner { + mouse_down_out_count: Rc>, + } + + impl Render for MouseDownOutOwner { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let mouse_down_out_count = self.mouse_down_out_count.clone(); + div() + .size_full() + .child(div().id("target").w(px(50.)).h(px(50.)).on_mouse_down_out( + move |_, _, _| { + *mouse_down_out_count.borrow_mut() += 1; + }, + )) + } + } + + #[test] + fn mouse_down_out_is_suppressed_while_window_prompt_is_active() { + let mut test_app = TestAppContext::single(); + let mouse_down_out_count = Rc::new(RefCell::new(0)); + let window = test_app.add_window({ + let mouse_down_out_count = mouse_down_out_count.clone(); + move |_, _| MouseDownOutOwner { + mouse_down_out_count, + } + }); + let any_window: AnyWindowHandle = window.into(); + + fn dispatch_mouse_down_outside_target( + test_app: &mut TestAppContext, + any_window: AnyWindowHandle, + ) { + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseDownEvent { + position: point(px(75.), px(75.)), + button: MouseButton::Left, + modifiers: Default::default(), + click_count: 1, + first_mouse: false, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + } + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + + dispatch_mouse_down_outside_target(&mut test_app, any_window); + assert_eq!( + *mouse_down_out_count.borrow(), + 1, + "mouse down outside the element should fire mouse-down-out listeners" + ); + + test_app + .update_window(any_window, |_, window, cx| { + cx.set_prompt_builder(crate::fallback_prompt_renderer); + let _receiver = + window.prompt(crate::PromptLevel::Warning, "message", None, &["Ok"], cx); + assert!(window.has_active_prompt()); + window.draw(cx).clear(cx); + }) + .unwrap(); + + dispatch_mouse_down_outside_target(&mut test_app, any_window); + assert_eq!( + *mouse_down_out_count.borrow(), + 1, + "mouse down over an active prompt should not fire mouse-down-out listeners" + ); + } + + #[test] + fn test_accessibility_id_builder_writes_author_id() { + let mut element = div() + .id("buffer-font-size") + .accessibility_id("settings.buffer-font-size"); + let mut node = accesskit::Node::new(accesskit::Role::SpinButton); + + element.interactivity().write_a11y_info(&mut node); + + assert_eq!(node.author_id(), Some("settings.buffer-font-size")); + } + + #[test] + fn test_write_a11y_info_string_and_numeric_properties() { + let mut interactivity = Interactivity::default(); + interactivity.aria.author_id = Some("settings.buffer-font-size".into()); + interactivity.aria.label = Some("Buffer Font Size".into()); + interactivity.aria.value = Some("15".into()); + interactivity.aria.placeholder = Some("Search".into()); + interactivity.aria.numeric_value = Some(15.0); + interactivity.aria.min_numeric_value = Some(6.0); + interactivity.aria.max_numeric_value = Some(72.0); + interactivity.aria.numeric_value_step = Some(1.0); + + let mut node = accesskit::Node::new(accesskit::Role::SpinButton); + interactivity.write_a11y_info(&mut node); + + assert_eq!(node.author_id(), Some("settings.buffer-font-size")); + assert_eq!(node.label(), Some("Buffer Font Size")); + assert_eq!(node.value(), Some("15")); + assert_eq!(node.placeholder(), Some("Search")); + assert_eq!(node.numeric_value(), Some(15.0)); + assert_eq!(node.min_numeric_value(), Some(6.0)); + assert_eq!(node.max_numeric_value(), Some(72.0)); + assert_eq!(node.numeric_value_step(), Some(1.0)); + } + + /// Two focusable, clickable elements ("a" and "b") used to exercise the + /// Enter/Space -> synthesized click press/release pairing. + struct KeyboardActivationTest { + focus_a: FocusHandle, + focus_b: FocusHandle, + clicks: Rc>>, + } + + impl Render for KeyboardActivationTest { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let clicks_a = self.clicks.clone(); + let clicks_b = self.clicks.clone(); + div() + .size_full() + .child( + div() + .id("a") + .w(px(50.)) + .h(px(50.)) + .track_focus(&self.focus_a) + .on_click(move |_, _, _| clicks_a.borrow_mut().push("a")), + ) + .child( + div() + .id("b") + .w(px(50.)) + .h(px(50.)) + .track_focus(&self.focus_b) + .on_click(move |_, _, _| clicks_b.borrow_mut().push("b")), + ) + } + } + + fn setup_keyboard_activation_test() -> ( + TestAppContext, + AnyWindowHandle, + Rc>>, + FocusHandle, + FocusHandle, + ) { + let mut cx = TestAppContext::single(); + let (focus_a, focus_b) = cx.update(|cx| (cx.focus_handle(), cx.focus_handle())); + let clicks: Rc>> = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let focus_a = focus_a.clone(); + let focus_b = focus_b.clone(); + let clicks = clicks.clone(); + move |_, _| KeyboardActivationTest { + focus_a, + focus_b, + clicks, + } + }); + (cx, window.into(), clicks, focus_a, focus_b) + } + + /// Move focus to `handle`, flush effects, then paint so the newly focused + /// element registers its key handlers for the next dispatched event. + fn focus_and_draw(cx: &mut TestAppContext, window: AnyWindowHandle, handle: &FocusHandle) { + cx.update_window(window, |_, window, cx| window.focus(handle, cx)) + .unwrap(); + cx.run_until_parked(); + cx.update_window(window, |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + } + + fn key_down(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) { + let keystroke = Keystroke::parse(key).unwrap(); + cx.update_window(window, |_, window, cx| { + window.dispatch_event( + KeyDownEvent { + keystroke, + is_held: false, + prefer_character_input: false, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + } + + fn key_up(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) { + let keystroke = Keystroke::parse(key).unwrap(); + cx.update_window(window, |_, window, cx| { + window.dispatch_event(KeyUpEvent { keystroke }.to_platform_input(), cx); + }) + .unwrap(); + } + + /// Pressing and releasing Enter on the same focused element fires a click. + #[test] + fn keyboard_activation_fires_click_on_same_element() { + let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); + + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "enter"); + key_up(&mut cx, window, "enter"); + + assert_eq!(*clicks.borrow(), vec!["a"]); + } + + /// A key-down whose key-up lands on a *different* element (because focus + /// moved in between) must not leak a synthesized click onto the newly + /// focused element. This is the core regression: previously the key-up + /// handler fired unconditionally on whatever was focused at key-up time. + #[test] + fn keyboard_activation_does_not_leak_across_focus_change() { + let (mut cx, window, clicks, focus_a, focus_b) = setup_keyboard_activation_test(); + + // Enter pressed while "a" is focused... + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "enter"); + + // ...focus moves to "b" before the release (as a confirm action would)... + focus_and_draw(&mut cx, window, &focus_b); + key_up(&mut cx, window, "enter"); + + // ...so neither element is clicked: "a" never saw the up, and "b" + // never saw the down. + assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); + } + + /// A keydown whose flag is left pending because focus moved away before + /// the keyup must not fire a click when focus later *returns* to the same + /// element (the menu trigger reopening case). The stamped focus generation + /// no longer matches, so the stale pending state is ignored. + #[test] + fn keyboard_activation_does_not_leak_when_focus_returns() { + let (mut cx, window, clicks, focus_a, focus_b) = setup_keyboard_activation_test(); + + // Enter pressed on "a"... + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "enter"); + + // ...focus leaves "a" before its keyup (so the pending state is never + // consumed), then comes back to "a"... + focus_and_draw(&mut cx, window, &focus_b); + focus_and_draw(&mut cx, window, &focus_a); + key_up(&mut cx, window, "enter"); + + // ...and the now-stale pending keydown must not fire a click. + assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); + } + + /// A non-activation key *released* during the press must cancel the pending + /// activation. For the sequence escape-down, space-down, escape-up, + /// space-up the space forms a clean down/up pair, but the intervening + /// escape-up means this isn't a plain space activation, so no click fires. + #[test] + fn keyboard_activation_cleared_by_intervening_key_release() { + let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); + + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "escape"); + key_down(&mut cx, window, "space"); + key_up(&mut cx, window, "escape"); + key_up(&mut cx, window, "space"); + + assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); + } + + /// The flag is a single activation marker, not keyed by which activation + /// key was used, so a Space down paired with an Enter up on the same + /// element still fires a click. + #[test] + fn keyboard_activation_does_not_distinguish_space_and_enter() { + let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); + + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "space"); + key_up(&mut cx, window, "enter"); + + assert_eq!(*clicks.borrow(), vec!["a"]); + } + + /// A non-activation key pressed between the activation down and up clears + /// the pending flag, suppressing the click. + #[test] + fn keyboard_activation_cleared_by_intervening_keydown() { + let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); + + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "enter"); + key_down(&mut cx, window, "a"); + key_up(&mut cx, window, "enter"); + + assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); + } + + /// A modified Enter (e.g. cmd-enter) is not treated as an activation key, + /// so it neither sets the pending flag nor fires a click on release. + #[test] + fn keyboard_activation_ignores_modified_keys() { + let (mut cx, window, clicks, focus_a, _focus_b) = setup_keyboard_activation_test(); + + focus_and_draw(&mut cx, window, &focus_a); + key_down(&mut cx, window, "cmd-enter"); + key_up(&mut cx, window, "cmd-enter"); + + assert!(clicks.borrow().is_empty(), "clicks: {:?}", clicks.borrow()); + } + + /// Two sibling tab groups, each a focusable container that is *not* itself a + /// tab stop and holds a single tab stop. Mirrors how the title bar and + /// status bar expose their controls as ARIA toolbars. + struct TabGroupFocus { + group_a: FocusHandle, + item_a: FocusHandle, + group_b: FocusHandle, + item_b: FocusHandle, + } + + impl Render for TabGroupFocus { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + fn group(container: &FocusHandle, item: &FocusHandle) -> Div { + div() + .track_focus(container) + .tab_group() + .child(div().track_focus(item)) + } + div() + .child(group(&self.group_a, &self.item_a)) + .child(group(&self.group_b, &self.item_b)) + } + } + + /// Focusing a tab-group container and pressing Tab (`focus_next`) must move + /// focus to the first tab stop *inside that container*, as documented on + /// [`InteractiveElement::tab_stop`]. + #[test] + fn focus_next_from_tab_group_container_enters_that_group() { + let mut cx = TestAppContext::single(); + let (group_a, item_a, group_b, item_b) = cx.update(|cx| { + ( + cx.focus_handle(), + cx.focus_handle().tab_stop(true), + cx.focus_handle(), + cx.focus_handle().tab_stop(true), + ) + }); + let window: AnyWindowHandle = cx + .add_window({ + let (group_a, item_a, group_b, item_b) = + (group_a, item_a, group_b.clone(), item_b.clone()); + move |_, _| TabGroupFocus { + group_a, + item_a, + group_b, + item_b, + } + }) + .into(); + cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + + // Focus the *second* group's container, then advance like Tab would. + let focused = cx + .update_window(window, |_, window, cx| { + window.focus(&group_b, cx); + window.focus_next(cx); + window.focused(cx).map(|handle| handle.id) + }) + .unwrap(); + + assert_eq!(focused, Some(item_b.id)); + } + + #[gpui::test] + fn test_fractional_padding_does_not_make_a_fitting_container_scrollable( + cx: &mut TestAppContext, + ) { + struct PaddedContainer { + scroll_handle: ScrollHandle, + } + + impl Render for PaddedContainer { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + // 4.25px of padding snaps to 4px in layout, so a 42px child + // fits the 50px box exactly. + div().size_full().child( + div() + .id("container") + .h(px(50.)) + .w(px(100.)) + .py(px(4.25)) + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .child(div().w_full().h(px(42.))), + ) + } + } + + let scroll_handle = ScrollHandle::new(); + let window: AnyWindowHandle = cx + .add_window({ + let scroll_handle = scroll_handle.clone(); + move |_, _| PaddedContainer { scroll_handle } + }) + .into(); + cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + + assert_eq!(scroll_handle.max_offset().y, px(0.)); + } + + struct ContentSizedGrid; + + impl Render for ContentSizedGrid { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let widths = [px(100.), px(200.), px(50.)]; + div().size_full().child( + div() + .w_full() + .grid() + .grid_cols_max_content(widths.len() as u16) + .children(widths.into_iter().enumerate().map(|(index, width)| { + div() + .debug_selector(move || format!("cell-{index}")) + .w(width) + .h(px(10.)) + })), + ) + } + } + + #[gpui::test] + fn grid_cols_max_content_sizes_columns_to_their_content(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| ContentSizedGrid); + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + + let mut bounds = |selector: &'static str| { + cx.update_window(window.into(), |_, window, _| { + window.rendered_frame.debug_bounds.get(selector).copied() + }) + .unwrap() + .unwrap_or_else(|| panic!("{selector} was not rendered")) + }; + + assert_eq!(bounds("cell-0").origin.x, px(0.)); + assert_eq!(bounds("cell-1").origin.x, px(100.)); + assert_eq!(bounds("cell-2").origin.x, px(300.)); + } +} diff --git a/vendor/gpui-pre/src/elements/image_cache.rs b/vendor/gpui-pre/src/elements/image_cache.rs new file mode 100644 index 0000000..8494814 --- /dev/null +++ b/vendor/gpui-pre/src/elements/image_cache.rs @@ -0,0 +1,330 @@ +use crate::{ + AnyElement, AnyEntity, App, AppContext, Asset, AssetLogger, Bounds, Element, ElementId, Entity, + GlobalElementId, ImageAssetLoader, ImageCacheError, InspectorElementId, IntoElement, LayoutId, + ParentElement, Pixels, RenderImage, Resource, Style, StyleRefinement, Styled, Window, hash, +}; + +use crate::asset_cache::CachedLoad; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{collections::HashMap, fmt, sync::Arc}; + +/// An image cache element, all its child img elements will use the cache specified by this element. +/// Note that this could as simple as passing an `Entity` +pub fn image_cache(image_cache_provider: impl ImageCacheProvider) -> ImageCacheElement { + ImageCacheElement { + image_cache_provider: Box::new(image_cache_provider), + style: StyleRefinement::default(), + children: SmallVec::default(), + } +} + +/// A dynamically typed image cache, which can be used to store any image cache +#[derive(Clone)] +pub struct AnyImageCache { + image_cache: AnyEntity, + load_fn: fn( + image_cache: &AnyEntity, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>>, +} + +impl From> for AnyImageCache { + fn from(image_cache: Entity) -> Self { + Self { + image_cache: image_cache.into_any(), + load_fn: any_image_cache::load::, + } + } +} + +impl AnyImageCache { + /// Load an image given a resource + /// returns the result of loading the image if it has finished loading, or None if it is still loading + pub fn load( + &self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + (self.load_fn)(&self.image_cache, resource, window, cx) + } +} + +mod any_image_cache { + use super::*; + + pub(crate) fn load( + image_cache: &AnyEntity, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + let image_cache = image_cache.clone().downcast::().unwrap(); + image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) + } +} + +/// An image cache element. +pub struct ImageCacheElement { + image_cache_provider: Box, + style: StyleRefinement, + children: SmallVec<[AnyElement; 2]>, +} + +impl ParentElement for ImageCacheElement { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Styled for ImageCacheElement { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl IntoElement for ImageCacheElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ImageCacheElement { + type RequestLayoutState = SmallVec<[LayoutId; 4]>; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let image_cache = self.image_cache_provider.provide(window, cx); + window.with_image_cache(Some(image_cache), |window| { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.request_layout(window, cx)) + .collect::>(); + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, child_layout_ids.iter().copied(), cx); + (layout_id, child_layout_ids) + }) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + for child in &mut self.children { + child.prepaint(window, cx); + } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let image_cache = self.image_cache_provider.provide(window, cx); + window.with_image_cache(Some(image_cache), |window| { + for child in &mut self.children { + child.paint(window, cx); + } + }) + } +} + +/// Owns an image load and its cached result. +/// +/// Dropping the entry cancels a pending load. Image caches must remove loaded +/// images from windows before discarding the entry. +pub struct ImageCacheItem(CachedLoad, ImageCacheError>>); + +impl std::fmt::Debug for ImageCacheItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ImageCacheItem") + .field("result", &self.get()) + .finish() + } +} + +impl ImageCacheItem { + /// Starts loading an image into a new cache entry. + pub fn new(source: &Resource, cx: &mut App) -> Self { + let future = AssetLogger::::load(source.clone(), cx); + Self(CachedLoad::new(future, cx)) + } + + /// Returns the cached result without subscribing to completion notifications. + pub fn get(&self) -> Option, ImageCacheError>> { + self.0.get() + } + + /// Returns the cached result or subscribes the current view to completion. + pub fn use_image(&self, window: &Window) -> Option, ImageCacheError>> { + self.0.use_by(window.current_view()) + } +} + +/// An object that can handle the caching and unloading of images. +/// Implementations of this trait should ensure that images are removed from all windows when they are no longer needed. +pub trait ImageCache: 'static { + /// Load an image given a resource + /// returns the result of loading the image if it has finished loading, or None if it is still loading + fn load( + &mut self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>>; +} + +/// An object that can create an ImageCache during the render phase. +/// See the ImageCache trait for more information. +pub trait ImageCacheProvider: 'static { + /// Called during the request_layout phase to create an ImageCache. + fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache; +} + +impl ImageCacheProvider for Entity { + fn provide(&mut self, _window: &mut Window, _cx: &mut App) -> AnyImageCache { + self.clone().into() + } +} + +/// An implementation of ImageCache, that uses an LRU caching strategy to unload images when the cache is full +pub struct RetainAllImageCache(HashMap); + +impl fmt::Debug for RetainAllImageCache { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HashMapImageCache") + .field("num_images", &self.0.len()) + .finish() + } +} + +impl RetainAllImageCache { + /// Create a new image cache. + #[inline] + pub fn new(cx: &mut App) -> Entity { + let e = cx.new(|_cx| RetainAllImageCache(HashMap::new())); + cx.observe_release(&e, |image_cache, cx| { + for (_, item) in std::mem::replace(&mut image_cache.0, HashMap::new()) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, None); + } + } + }) + .detach(); + e + } + + /// Load an image from the given source. + /// + /// Returns `None` if the image is loading. + pub fn load( + &mut self, + source: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + let hash = hash(source); + + self.0 + .entry(hash) + .or_insert_with(|| ImageCacheItem::new(source, cx)) + .use_image(window) + } + + /// Clear the image cache. + pub fn clear(&mut self, window: &mut Window, cx: &mut App) { + for (_, item) in std::mem::replace(&mut self.0, HashMap::new()) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, Some(window)); + } + } + } + + /// Remove the image from the cache by the given source. + pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) { + let hash = hash(source); + if let Some(item) = self.0.remove(&hash) + && let Some(Ok(image)) = item.get() + { + cx.drop_image(image, Some(window)); + } + } + + /// Returns the number of images in the cache. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl ImageCache for RetainAllImageCache { + fn load( + &mut self, + resource: &Resource, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + RetainAllImageCache::load(self, resource, window, cx) + } +} + +/// Constructs a retain-all image cache that uses the element state associated with the given ID. +pub fn retain_all(id: impl Into) -> RetainAllImageCacheProvider { + RetainAllImageCacheProvider { id: id.into() } +} + +/// A provider struct for creating a retain-all image cache inline +pub struct RetainAllImageCacheProvider { + id: ElementId, +} + +impl ImageCacheProvider for RetainAllImageCacheProvider { + fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache { + window + .with_global_id(self.id.clone(), |global_id, window| { + window.with_element_state::, _>( + global_id, + |cache, _window| { + let mut cache = cache.unwrap_or_else(|| RetainAllImageCache::new(cx)); + (cache.clone(), cache) + }, + ) + }) + .into() + } +} diff --git a/vendor/gpui-pre/src/elements/img.rs b/vendor/gpui-pre/src/elements/img.rs new file mode 100644 index 0000000..2c77e5f --- /dev/null +++ b/vendor/gpui-pre/src/elements/img.rs @@ -0,0 +1,983 @@ +use crate::{ + AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId, + Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement, + Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, + SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image, + decode_static_image_from_decoder, px, +}; +use anyhow::Result; + +use futures::Future; +use gpui_util::ResultExt; +use image::{ + AnimationDecoder, ImageError, ImageFormat, Rgba, + codecs::{gif::GifDecoder, webp::WebPDecoder}, +}; +use scheduler::Instant; +use smallvec::SmallVec; +use std::{ + fs, + io::{self, Cursor}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::Duration, +}; +use thiserror::Error; + +use super::{Stateful, StatefulInteractiveElement}; + +/// The delay before showing the loading state. +pub const LOADING_DELAY: Duration = Duration::from_millis(200); + +/// A type alias to the resource loader that the `img()` element uses. +/// +/// Note: that this is only for Resources, like URLs or file paths. +/// Custom loaders, or external images will not use this asset loader +pub type ImgResourceLoader = AssetLogger; + +/// A source of image content. +#[derive(Clone)] +pub enum ImageSource { + /// The image content will be loaded from some resource location + Resource(Resource), + /// Cached image data + Render(Arc), + /// Cached image data + Image(Arc), + /// A custom loading function to use + Custom(Arc Option, ImageCacheError>>>), +} + +fn is_uri(uri: &str) -> bool { + url::Url::from_str(uri).is_ok() +} + +impl From for ImageSource { + fn from(value: SharedUri) -> Self { + Self::Resource(Resource::Uri(value)) + } +} + +impl<'a> From<&'a str> for ImageSource { + fn from(s: &'a str) -> Self { + if is_uri(s) { + Self::Resource(Resource::Uri(s.to_string().into())) + } else { + Self::Resource(Resource::Embedded(s.to_string().into())) + } + } +} + +impl From for ImageSource { + fn from(s: String) -> Self { + if is_uri(&s) { + Self::Resource(Resource::Uri(s.into())) + } else { + Self::Resource(Resource::Embedded(s.into())) + } + } +} + +impl From for ImageSource { + fn from(s: SharedString) -> Self { + s.as_ref().into() + } +} + +impl From<&Path> for ImageSource { + fn from(value: &Path) -> Self { + Self::Resource(value.to_path_buf().into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Resource(value.into()) + } +} + +impl From for ImageSource { + fn from(value: PathBuf) -> Self { + Self::Resource(value.into()) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Render(value) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Image(value) + } +} + +impl From for ImageSource +where + F: Fn(&mut Window, &mut App) -> Option, ImageCacheError>> + 'static, +{ + fn from(value: F) -> Self { + Self::Custom(Arc::new(value)) + } +} + +/// The style of an image element. +pub struct ImageStyle { + grayscale: bool, + object_fit: ObjectFit, + loading: Option AnyElement>>, + fallback: Option AnyElement>>, +} + +impl Default for ImageStyle { + fn default() -> Self { + Self { + grayscale: false, + object_fit: ObjectFit::Contain, + loading: None, + fallback: None, + } + } +} + +/// Style an image element. +pub trait StyledImage: Sized { + /// Get a mutable [ImageStyle] from the element. + fn image_style(&mut self) -> &mut ImageStyle; + + /// Set the image to be displayed in grayscale. + fn grayscale(mut self, grayscale: bool) -> Self { + self.image_style().grayscale = grayscale; + self + } + + /// Set the object fit for the image. + fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.image_style().object_fit = object_fit; + self + } + + /// Set a fallback function that will be invoked to render an error view should + /// the image fail to load. + fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().fallback = Some(Box::new(fallback)); + self + } + + /// Set a fallback function that will be invoked to render a view while the image + /// is still being loaded. + fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { + self.image_style().loading = Some(Box::new(loading)); + self + } +} + +impl StyledImage for Img { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.style + } +} + +impl StyledImage for Stateful { + fn image_style(&mut self) -> &mut ImageStyle { + &mut self.element.style + } +} + +/// An image element. +pub struct Img { + interactivity: Interactivity, + source: ImageSource, + style: ImageStyle, + image_cache: Option, +} + +/// Create a new image element. +#[track_caller] +pub fn img(source: impl Into) -> Img { + Img { + interactivity: Interactivity::new(), + source: source.into(), + style: ImageStyle::default(), + image_cache: None, + } +} + +impl Img { + /// A list of all format extensions currently supported by this img element + pub fn extensions() -> &'static [&'static str] { + // This is the list in [image::ImageFormat::from_extension] + `svg` + &[ + "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", + "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", + ] + } + + /// Sets the image cache for the current node. + /// + /// If the `image_cache` is not explicitly provided, the function will determine the image cache by: + /// + /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used. + /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback. + /// + /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed, + /// while ensuring a default behavior when no cache is explicitly specified. + #[inline] + pub fn image_cache(self, image_cache: &Entity) -> Self { + Self { + image_cache: Some(image_cache.clone().into()), + ..self + } + } +} + +impl Deref for Stateful { + type Target = Img; + + fn deref(&self) -> &Self::Target { + &self.element + } +} + +impl DerefMut for Stateful { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.element + } +} + +/// The image state between frames +struct ImgState { + frame_index: usize, + last_frame_time: Option, + started_loading: Option<(Instant, Task<()>)>, +} + +/// The image layout state between frames +pub struct ImgLayoutState { + frame_index: usize, + replacement: Option, +} + +impl Element for Img { + type RequestLayoutState = ImgLayoutState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + self.interactivity.source_location() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut layout_state = ImgLayoutState { + frame_index: 0, + replacement: None, + }; + + window.with_optional_element_state(global_id, |state, window| { + let mut state = state.map(|state| { + state.unwrap_or(ImgState { + frame_index: 0, + last_frame_time: None, + started_loading: None, + }) + }); + + let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0); + + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |mut style, window, cx| { + let mut replacement_id = None; + + match self.source.use_data( + self.image_cache + .clone() + .or_else(|| window.image_cache_stack.last().cloned()), + window, + cx, + ) { + Some(Ok(data)) => { + let frame_count = data.frame_count(); + let max_frame_index = frame_count.saturating_sub(1); + + if let Some(state) = &mut state { + state.frame_index = state.frame_index.min(max_frame_index); + if frame_count > 1 && !cx.reduce_motion() { + if window.is_window_active() { + let current_time = Instant::now(); + if let Some(last_frame_time) = state.last_frame_time { + let elapsed = current_time - last_frame_time; + let frame_duration = + Duration::from(data.delay(state.frame_index)); + + if elapsed >= frame_duration { + state.frame_index = + (state.frame_index + 1) % frame_count; + state.last_frame_time = + Some(current_time - (elapsed - frame_duration)); + } + } else { + state.last_frame_time = Some(current_time); + } + } else { + state.last_frame_time = None; + } + } else { + state.last_frame_time = None; + } + state.started_loading = None; + frame_index = state.frame_index; + } + + let image_size = data.render_size(frame_index); + + if style.aspect_ratio.is_none() { + style.aspect_ratio = Some(image_size.width / image_size.height); + } + + if let Length::Auto = style.size.width { + style.size.width = match style.size.height { + Length::Definite(DefiniteLength::Absolute(abs_length)) => { + let height_px = abs_length.to_pixels(window.rem_size()); + Length::Definite( + px(image_size.width.0 * height_px.0 + / image_size.height.0) + .into(), + ) + } + _ => Length::Definite(image_size.width.into()), + }; + } + + if let Length::Auto = style.size.height { + style.size.height = match style.size.width { + Length::Definite(DefiniteLength::Absolute(abs_length)) => { + let width_px = abs_length.to_pixels(window.rem_size()); + Length::Definite( + px(image_size.height.0 * width_px.0 + / image_size.width.0) + .into(), + ) + } + _ => Length::Definite(image_size.height.into()), + }; + } + + if global_id.is_some() + && data.frame_count() > 1 + && window.is_window_active() + && !cx.reduce_motion() + { + window.request_animation_frame(); + } + } + Some(_err) => { + if let Some(fallback) = self.style.fallback.as_ref() { + let mut element = fallback(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); + } + if let Some(state) = &mut state { + state.started_loading = None; + } + } + None => { + if let Some(state) = &mut state { + if let Some((started_loading, _)) = state.started_loading { + if started_loading.elapsed() > LOADING_DELAY + && let Some(loading) = self.style.loading.as_ref() + { + let mut element = loading(); + replacement_id = Some(element.request_layout(window, cx)); + layout_state.replacement = Some(element); + } + } else { + let current_view = window.current_view(); + let task = window.spawn(cx, async move |cx| { + cx.background_executor().timer(LOADING_DELAY).await; + cx.update(move |_, cx| { + cx.notify(current_view); + }) + .ok(); + }); + state.started_loading = Some((Instant::now(), task)); + } + } + } + } + + window.request_layout(style, replacement_id, cx) + }, + ); + + layout_state.frame_index = frame_index; + + ((layout_id, layout_state), state) + }) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_, _, hitbox, window, cx| { + if let Some(replacement) = &mut request_layout.replacement { + replacement.prepaint(window, cx); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + layout_state: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let source = self.source.clone(); + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + if let Some(Ok(data)) = source.use_data( + self.image_cache + .clone() + .or_else(|| window.image_cache_stack.last().cloned()), + window, + cx, + ) { + if data.frame_count() == 0 { + return; + } + let new_bounds = self + .style + .object_fit + .get_bounds(bounds, data.size(layout_state.frame_index)); + let corner_radii = style.corner_radii.to_pixels(window.rem_size()); + window + .paint_image( + bounds, + new_bounds, + corner_radii, + data, + layout_state.frame_index, + self.style.grayscale, + ) + .log_err(); + } else if let Some(replacement) = &mut layout_state.replacement { + replacement.paint(window, cx); + } + }, + ) + } +} + +impl Styled for Img { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Img { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +impl IntoElement for Img { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl StatefulInteractiveElement for Img {} + +impl ImageSource { + pub(crate) fn use_data( + &self, + cache: Option, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + match self { + ImageSource::Resource(resource) => { + if let Some(cache) = cache { + cache.load(resource, window, cx) + } else { + window.use_asset::(resource, cx) + } + } + ImageSource::Custom(loading_fn) => loading_fn(window, cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => window.use_asset::>(data, cx), + } + } + + pub(crate) fn get_data( + &self, + cache: Option, + window: &mut Window, + cx: &mut App, + ) -> Option, ImageCacheError>> { + match self { + ImageSource::Resource(resource) => { + if let Some(cache) = cache { + cache.load(resource, window, cx) + } else { + window.get_asset::(resource, cx) + } + } + ImageSource::Custom(loading_fn) => loading_fn(window, cx), + ImageSource::Render(data) => Some(Ok(data.to_owned())), + ImageSource::Image(data) => window.get_asset::>(data, cx), + } + } + + /// Remove this image source from the asset system + pub fn remove_asset(&self, cx: &mut App) { + match self { + ImageSource::Resource(resource) => { + cx.remove_asset::(resource); + } + ImageSource::Custom(_) | ImageSource::Render(_) => {} + ImageSource::Image(data) => cx.remove_asset::>(data), + } + } + + /// Check whether this image source is present in the asset system (loading + /// or loaded), without fetching it. + #[cfg(any(test, feature = "test-support"))] + pub fn is_asset_cached(&self, cx: &App) -> bool { + match self { + ImageSource::Resource(resource) => cx.has_asset::(resource), + ImageSource::Custom(_) | ImageSource::Render(_) => false, + ImageSource::Image(data) => cx.has_asset::>(data), + } + } +} + +#[derive(Clone)] +enum ImageDecoder {} + +impl Asset for ImageDecoder { + type Source = Arc; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let renderer = cx.svg_renderer(); + async move { source.to_image_data(renderer).map_err(Into::into) } + } +} + +/// An image loader for the GPUI asset system +#[derive(Clone)] +pub enum ImageAssetLoader {} + +impl Asset for ImageAssetLoader { + type Source = Resource; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut App, + ) -> impl Future + Send + 'static { + let client = cx.http_client(); + // TODO: Can we make SVGs always rescale? + // let scale_factor = cx.scale_factor(); + let svg_renderer = cx.svg_renderer(); + let asset_source = cx.asset_source().clone(); + async move { + let bytes = match source.clone() { + Resource::Path(uri) => fs::read(uri.as_ref())?, + Resource::Uri(uri) => { + use anyhow::Context as _; + use futures::AsyncReadExt as _; + + let mut response = client + .get(uri.as_ref(), ().into(), true) + .await + .with_context(|| format!("loading image asset from {uri:?}"))?; + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + if !response.status().is_success() { + let mut body = String::from_utf8_lossy(&body).into_owned(); + let first_line = body.lines().next().unwrap_or("").trim_end(); + body.truncate(first_line.len()); + return Err(ImageCacheError::BadStatus { + uri, + status: response.status(), + body, + }); + } + body + } + Resource::Embedded(path) => { + let data = asset_source.load(&path).ok().flatten(); + if let Some(data) = data { + data.to_vec() + } else { + return Err(ImageCacheError::Asset( + format!("Embedded resource not found: {}", path).into(), + )); + } + } + }; + + if let Ok(format) = image::guess_format(&bytes) { + let data = match format { + ImageFormat::Gif => { + let decoder = GifDecoder::new(Cursor::new(&bytes))?; + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + match frame { + Ok(mut frame) => { + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + Err(err) => { + log::debug!( + "Skipping GIF frame in {source:?} due to decode error: {err}" + ); + } + } + } + + if frames.is_empty() { + return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!( + "GIF could not be decoded: all frames failed ({source:?})" + )))); + } + + frames + } + ImageFormat::WebP => { + let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?; + + if decoder.has_animation() { + let _ = decoder.set_background_color(Rgba([0, 0, 0, 0])); + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + match frame { + Ok(mut frame) => { + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + Err(err) => { + log::debug!( + "Skipping WebP frame in {source:?} due to decode error: {err}" + ); + } + } + } + + if frames.is_empty() { + return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!( + "WebP could not be decoded: all frames failed ({source:?})" + )))); + } + + frames + } else { + decode_static_image_from_decoder(decoder)? + } + } + _ => decode_static_image(&bytes, format)?, + }; + + Ok(Arc::new(RenderImage::new(data))) + } else { + svg_renderer + .render_single_frame(&bytes, 1.0) + .map_err(Into::into) + } + } + } +} + +/// An error that can occur when interacting with the image cache. +#[derive(Debug, Error, Clone)] +pub enum ImageCacheError { + /// Some other kind of error occurred + #[error("error: {0}")] + Other(#[from] Arc), + /// An error that occurred while reading the image from disk. + #[error("IO error: {0}")] + Io(Arc), + /// An error that occurred while processing an image. + #[error("unexpected http status for {uri}: {status}, body: {body}")] + BadStatus { + /// The URI of the image. + uri: SharedUri, + /// The HTTP status code. + status: http_client::StatusCode, + /// The HTTP response body. + body: String, + }, + /// An error that occurred while processing an asset. + #[error("asset error: {0}")] + Asset(SharedString), + /// An error that occurred while processing an image. + #[error("image error: {0}")] + Image(Arc), + /// An error that occurred while processing an SVG. + #[error("svg error: {0}")] + Usvg(Arc), +} + +impl From for ImageCacheError { + fn from(value: anyhow::Error) -> Self { + Self::Other(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: io::Error) -> Self { + Self::Io(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: usvg::Error) -> Self { + Self::Usvg(Arc::new(value)) + } +} + +impl From for ImageCacheError { + fn from(value: image::ImageError) -> Self { + Self::Image(Arc::new(value)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size}; + use image::{Frame, ImageBuffer, Rgba}; + + const TEST_IMG_ID: &str = "test-img"; + + fn test_image(frame_count: usize) -> Arc { + let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0]))); + Arc::new(RenderImage::new(SmallVec::from_iter( + (0..frame_count).map(|_| frame.clone()), + ))) + } + + fn test_image_with_size(width: u32, height: u32) -> Arc { + let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0]))); + Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1))) + } + + /// Overwrites the cached `frame_index` of the sibling `img` during paint. + fn seed_frame_index(frame_index: usize) -> impl IntoElement { + canvas( + |_, _, _| (), + move |_, _, window, _| { + window.with_global_id(TEST_IMG_ID.into(), |id, window| { + window.with_element_state::(id, |state, _| { + let mut state = state.expect("img state should be initialized"); + state.frame_index = frame_index; + ((), state) + }); + }); + }, + ) + } + + #[gpui::test] + fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) { + cx.add_empty_window() + .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(test_image(0))).into_any_element() + }); + } + + #[gpui::test] + fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + let image = test_image_with_size(200, 100); + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(image.clone())) + .size_full() + .object_fit(ObjectFit::Fill) + .into_any_element() + }); + let full_tile_bounds = window.update(|window, _| { + window + .rendered_frame + .scene + .polychrome_sprites + .last() + .expect("fill image should paint a sprite") + .tile + .bounds + }); + + window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(image)) + .size_full() + .object_fit(ObjectFit::Cover) + .into_any_element() + }); + + let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| { + let sprite = window + .rendered_frame + .scene + .polychrome_sprites + .last() + .expect("cover image should paint a sprite"); + (sprite.bounds, sprite.tile.bounds, window.scale_factor()) + }); + assert_eq!( + rendered_bounds, + Bounds { + origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)), + size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)), + } + ); + assert_eq!( + ( + rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0, + rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0, + rendered_tile_bounds.size.width.0, + rendered_tile_bounds.size.height.0, + ), + (50, 0, 100, 100), + ); + } + + #[gpui::test] + fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + + // A portrait image in a square container + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + div() + .size(px(100.)) + .overflow_hidden() + .child( + img(ImageSource::Render(test_image_with_size(100, 200))) + .size_full() + .aspect_square() + .object_fit(ObjectFit::Contain), + ) + .into_any_element() + }); + + let (rendered_bounds, scale_factor) = window.update(|window, _| { + let sprite = window + .rendered_frame + .scene + .polychrome_sprites + .last() + .expect("contained image should paint a sprite"); + (sprite.bounds, window.scale_factor()) + }); + + // The element stays 100x100, so the image is letterboxed horizontally + assert_eq!( + rendered_bounds, + Bounds { + origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)), + size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)), + } + ); + } + + #[gpui::test] + fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(test_image_with_size(200, 100))) + .size_full() + .rounded(px(100.)) + .object_fit(ObjectFit::Cover) + .into_any_element() + }); + + let (corner_radius, expected_corner_radius) = window.update(|window, _| { + ( + window + .rendered_frame + .scene + .polychrome_sprites + .last() + .map(|sprite| sprite.corner_radii.top_left), + px(50.).scale(window.scale_factor()), + ) + }); + assert_eq!(corner_radius, Some(expected_corner_radius)); + } + + #[gpui::test] + fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) { + let window = cx.add_empty_window(); + + // Assert that a cached frame_index from a previous multi-frame image + // does not cause an out-of-bounds panic when the image is replaced + // with one that has fewer frames. + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + div() + .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID)) + .child(seed_frame_index(4)) + .into_any_element() + }); + window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + img(ImageSource::Render(test_image(1))) + .id(TEST_IMG_ID) + .into_any_element() + }); + } +} diff --git a/vendor/gpui-pre/src/elements/list.rs b/vendor/gpui-pre/src/elements/list.rs new file mode 100644 index 0000000..fa21d10 --- /dev/null +++ b/vendor/gpui-pre/src/elements/list.rs @@ -0,0 +1,2973 @@ +//! A list element that can be used to render a large number of differently sized elements +//! efficiently. Clients of this API need to ensure that elements outside of the scrolled +//! area do not change their height for this element to function correctly. If your elements +//! do change height, notify the list element via [`ListState::splice`] or [`ListState::reset`]. +//! In order to minimize re-renders, this element's state is stored intrusively +//! on your own views, so that your code can coordinate directly with the list element's cached state. +//! +//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId, + FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, + Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, + Window, point, px, size, +}; +use collections::VecDeque; +use refineable::Refineable as _; +use std::{cell::RefCell, ops::Range, rc::Rc}; +use sum_tree::{Bias, Dimensions, SumTree}; + +type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; + +/// Construct a new list element +pub fn list( + state: ListState, + render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, +) -> List { + List { + state, + render_item: Box::new(render_item), + style: StyleRefinement::default(), + sizing_behavior: ListSizingBehavior::default(), + } +} + +/// A list element +pub struct List { + state: ListState, + render_item: Box, + style: StyleRefinement, + sizing_behavior: ListSizingBehavior, +} + +impl List { + /// Set the sizing behavior for the list. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } +} + +/// The list state that views must hold on behalf of the list element. +#[derive(Clone)] +pub struct ListState(Rc>); + +impl std::fmt::Debug for ListState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ListState") + } +} + +struct StateInner { + last_layout_bounds: Option>, + last_padding: Option>, + items: SumTree, + logical_scroll_top: Option, + alignment: ListAlignment, + overdraw: Pixels, + reset: bool, + #[allow(clippy::type_complexity)] + scroll_handler: Option>, + scrollbar_drag_start_height: Option, + measuring_behavior: ListMeasuringBehavior, + pending_scroll: Option, + follow_state: FollowState, +} + +/// Deferred scroll adjustment applied after the scroll-top item has been remeasured. +/// +/// An absolute pending scroll preserves the same pixel offset into the item, which keeps +/// visible text stable while content is appended to or removed from that item. A +/// proportional pending scroll preserves the same fractional position within the item, +/// which is useful when the whole list is being resized and each item scales similarly. +#[derive(Clone)] +enum PendingScroll { + /// Preserve the same pixel offset into the item after it is remeasured. + Absolute { item_ix: usize, offset: Pixels }, + /// Preserve the same fractional offset into the item after it is remeasured. + Proportional(PendingScrollFraction), +} + +/// Keeps track of a fractional scroll position within an item for restoration +/// after remeasurement. +#[derive(Clone)] +struct PendingScrollFraction { + /// The index of the item to scroll within. + item_ix: usize, + /// Fractional offset (0.0 to 1.0) within the item's height. + fraction: f32, +} + +/// Determines how remeasurement preserves the scroll position when the scroll-top item +/// changes height. +enum ScrollAnchor { + /// Preserve the same pixel offset into the scroll-top item. + Absolute, + /// Preserve the same fractional position within the scroll-top item. + Proportional, +} + +/// Controls whether the list automatically follows new content at the end. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum FollowMode { + /// Normal scrolling — no automatic following. + #[default] + Normal, + /// The list should auto-scroll along with the tail, when scrolled to bottom. + Tail, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum FollowState { + #[default] + Normal, + Tail { + is_following: bool, + }, +} + +impl FollowState { + fn is_following(&self) -> bool { + matches!(self, FollowState::Tail { is_following: true }) + } + + fn has_stopped_following(&self) -> bool { + matches!( + self, + FollowState::Tail { + is_following: false + } + ) + } + + fn start_following(&mut self) { + if let FollowState::Tail { + is_following: false, + } = self + { + *self = FollowState::Tail { is_following: true }; + } + } + + fn stop_following(&mut self) { + if let FollowState::Tail { is_following: true } = self { + *self = FollowState::Tail { + is_following: false, + }; + } + } +} + +/// Whether the list is scrolling from top to bottom or bottom to top. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ListAlignment { + /// The list is scrolling from top to bottom, like most lists. + Top, + /// The list is scrolling from bottom to top, like a chat log. + Bottom, +} + +/// A scroll event that has been converted to be in terms of the list's items. +pub struct ListScrollEvent { + /// The range of items currently visible in the list, after applying the scroll event. + pub visible_range: Range, + + /// The number of items that are currently visible in the list, after applying the scroll event. + pub count: usize, + + /// Whether the list has been scrolled. + pub is_scrolled: bool, + + /// Whether the list is currently in follow-tail mode (auto-scrolling to end). + pub is_following_tail: bool, +} + +/// The sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListSizingBehavior { + /// The list should calculate its size based on the size of its items. + Infer, + /// The list should not calculate a fixed size. + #[default] + Auto, +} + +/// The measuring behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListMeasuringBehavior { + /// Measure all items in the list. + /// Note: This can be expensive for the first frame in a large list. + Measure(bool), + /// Only measure visible items + #[default] + Visible, +} + +impl ListMeasuringBehavior { + fn reset(&mut self) { + match self { + ListMeasuringBehavior::Measure(has_measured) => *has_measured = false, + ListMeasuringBehavior::Visible => {} + } + } +} + +/// The horizontal sizing behavior to apply during layout. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ListHorizontalSizingBehavior { + /// List items' width can never exceed the width of the list. + #[default] + FitList, + /// List items' width may go over the width of the list, if any item is wider. + Unconstrained, +} + +struct LayoutItemsResponse { + max_item_width: Pixels, + scroll_top: ListOffset, + item_layouts: VecDeque, +} + +struct ItemLayout { + index: usize, + element: AnyElement, + size: Size, +} + +/// Frame state used by the [List] element after layout. +pub struct ListPrepaintState { + hitbox: Hitbox, + layout: LayoutItemsResponse, +} + +#[derive(Clone)] +enum ListItem { + Unmeasured { + size_hint: Option>, + focus_handle: Option, + }, + Measured { + size: Size, + focus_handle: Option, + }, +} + +impl ListItem { + fn size(&self) -> Option> { + if let ListItem::Measured { size, .. } = self { + Some(*size) + } else { + None + } + } + + fn size_hint(&self) -> Option> { + match self { + ListItem::Measured { size, .. } => Some(*size), + ListItem::Unmeasured { size_hint, .. } => *size_hint, + } + } + + fn focus_handle(&self) -> Option { + match self { + ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => { + focus_handle.clone() + } + } + } + + fn contains_focused(&self, window: &Window, cx: &App) -> bool { + match self { + ListItem::Unmeasured { focus_handle, .. } | ListItem::Measured { focus_handle, .. } => { + focus_handle + .as_ref() + .is_some_and(|handle| handle.contains_focused(window, cx)) + } + } + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ListItemSummary { + count: usize, + rendered_count: usize, + unrendered_count: usize, + height: Pixels, + has_focus_handles: bool, + has_unknown_height: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct Count(usize); + +#[derive(Clone, Debug, Default)] +struct Height(Pixels); + +impl ListState { + /// Construct a new list state, for storage on a view. + /// + /// The overdraw parameter controls how much extra space is rendered + /// above and below the visible area. Elements within this area will + /// be measured even though they are not visible. This can help ensure + /// that the list doesn't flicker or pop in when scrolling. + pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self { + let this = Self(Rc::new(RefCell::new(StateInner { + last_layout_bounds: None, + last_padding: None, + items: SumTree::default(), + logical_scroll_top: None, + alignment, + overdraw, + scroll_handler: None, + reset: false, + scrollbar_drag_start_height: None, + measuring_behavior: ListMeasuringBehavior::default(), + pending_scroll: None, + follow_state: FollowState::default(), + }))); + this.splice(0..0, item_count); + this + } + + /// Set the list to measure all items in the list in the first layout phase. + /// + /// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements. + pub fn measure_all(self) -> Self { + self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false); + self + } + + /// Pre-populate every unmeasured item with a uniform height hint so the scrollbar thumb + /// is correctly sized from the first frame, without measuring all items up front. + /// + /// As items are actually rendered their real heights replace the hint, so the scrollbar + /// converges to the exact size over time. This is a cheaper alternative to [`Self::measure_all`] + /// for lists where items have roughly uniform heights (e.g. table rows). + pub fn with_uniform_item_height(self, height: Pixels) -> Self { + self.apply_uniform_item_height(height); + self + } + + /// Reset this instantiation of the list state. + /// + /// Note that this will cause scroll events to be dropped until the next paint. + pub fn reset(&self, element_count: usize) { + let old_count = { + let state = &mut *self.0.borrow_mut(); + state.reset = true; + state.measuring_behavior.reset(); + state.logical_scroll_top = None; + state.pending_scroll = None; + state.scrollbar_drag_start_height = None; + state.items.summary().count + }; + + self.splice(0..old_count, element_count); + } + + /// Reset the list to `element_count` items, pre-populating every item with a + /// uniform height hint so the scrollbar thumb is correctly sized from the first + /// frame even for off-screen items. + pub fn reset_with_uniform_height(&self, element_count: usize, height: Pixels) { + self.reset(element_count); + self.apply_uniform_item_height(height); + } + + fn apply_uniform_item_height(&self, height: Pixels) { + let size_hint = Size { + width: px(0.), + height, + }; + let mut state = self.0.borrow_mut(); + let new_items = state + .items + .iter() + .map(|item| ListItem::Unmeasured { + size_hint: Some(item.size_hint().unwrap_or(size_hint)), + focus_handle: item.focus_handle(), + }) + .collect::>(); + let mut tree = SumTree::default(); + tree.extend(new_items, ()); + state.items = tree; + } + + /// Remeasure all items while preserving proportional scroll position. + /// + /// Use this when item heights may have changed (e.g., font size changes) + /// but the number and identity of items remains the same. + pub fn remeasure(&self) { + let count = self.item_count(); + self.remeasure_items_with_scroll_anchor(0..count, ScrollAnchor::Proportional); + } + + /// Mark items in `range` as needing remeasurement while preserving + /// the current scroll position. Unlike [`Self::splice`], this does + /// not change the number of items or blow away `logical_scroll_top`. + /// + /// Use this when an item's content has changed and its rendered + /// height may be different (e.g., streaming text, tool results + /// loading), but the item itself still exists at the same index. + pub fn remeasure_items(&self, range: Range) { + self.remeasure_items_with_scroll_anchor(range, ScrollAnchor::Absolute); + } + + fn remeasure_items_with_scroll_anchor(&self, range: Range, scroll_anchor: ScrollAnchor) { + let state = &mut *self.0.borrow_mut(); + + if let Some(scroll_top) = state.logical_scroll_top { + if range.contains(&scroll_top.item_ix) { + state.pending_scroll = match scroll_anchor { + ScrollAnchor::Absolute => Some(PendingScroll::Absolute { + item_ix: scroll_top.item_ix, + offset: scroll_top.offset_in_item, + }), + ScrollAnchor::Proportional => { + // If the scroll-top item falls within the remeasured range, + // store a fractional offset so the layout can restore the + // proportional scroll position after the item is re-rendered + // at its new height. + let mut cursor = state.items.cursor::(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + + cursor + .item() + .and_then(|item| { + item.size().map(|size| { + let fraction = if size.height.0 > 0.0 { + (scroll_top.offset_in_item.0 / size.height.0) + .clamp(0.0, 1.0) + } else { + 0.0 + }; + + PendingScroll::Proportional(PendingScrollFraction { + item_ix: scroll_top.item_ix, + fraction, + }) + }) + }) + .or_else(|| state.pending_scroll.clone()) + } + }; + } + } + + // Rebuild the tree, replacing items in the range with + // Unmeasured copies that keep their focus handles. + let new_items = { + let mut cursor = state.items.cursor::(()); + let mut new_items = cursor.slice(&Count(range.start), Bias::Right); + let invalidated = cursor.slice(&Count(range.end), Bias::Right); + new_items.extend( + invalidated.iter().map(|item| ListItem::Unmeasured { + size_hint: item.size_hint(), + focus_handle: item.focus_handle(), + }), + (), + ); + new_items.append(cursor.suffix(), ()); + new_items + }; + state.items = new_items; + state.measuring_behavior.reset(); + } + + /// The number of items in this list. + pub fn item_count(&self) -> usize { + self.0.borrow().items.summary().count + } + + /// Whether the list is scrolled to the end, or `None` if the list is + /// not scrollable or the total content height is not yet known. + pub fn is_scrolled_to_end(&self) -> Option { + let state = self.0.borrow(); + let bounds = state.last_layout_bounds?; + let summary = state.items.summary(); + if summary.has_unknown_height { + return None; + } + let padding = state.last_padding.unwrap_or_default(); + let content_height = summary.height + padding.top + padding.bottom; + let scroll_max = (content_height - bounds.size.height).max(px(0.)); + if scroll_max <= px(0.) { + return None; + } + let scroll_top = state.scroll_top(&state.logical_scroll_top()); + Some(scroll_top >= scroll_max) + } + + /// Inform the list state that the items in `old_range` have been replaced + /// by `count` new items that must be recalculated. + pub fn splice(&self, old_range: Range, count: usize) { + self.splice_focusable(old_range, (0..count).map(|_| None)) + } + + /// Register with the list state that the items in `old_range` have been replaced + /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles + /// to be supplied to properly integrate with items in the list that can be focused. If a focused item + /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. + pub fn splice_focusable( + &self, + old_range: Range, + focus_handles: impl IntoIterator>, + ) { + let state = &mut *self.0.borrow_mut(); + + let mut old_items = state.items.cursor::(()); + let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right); + old_items.seek_forward(&Count(old_range.end), Bias::Right); + + let mut spliced_count = 0; + new_items.extend( + focus_handles.into_iter().map(|focus_handle| { + spliced_count += 1; + ListItem::Unmeasured { + size_hint: None, + focus_handle, + } + }), + (), + ); + new_items.append(old_items.suffix(), ()); + drop(old_items); + state.items = new_items; + + if let Some(ListOffset { + item_ix, + offset_in_item, + }) = state.logical_scroll_top.as_mut() + { + if old_range.contains(item_ix) { + *item_ix = old_range.start; + *offset_in_item = px(0.); + } else if old_range.end <= *item_ix { + *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count; + } + } + } + + /// Set a handler that will be called when the list is scrolled. + pub fn set_scroll_handler( + &self, + handler: impl FnMut(&ListScrollEvent, &mut Window, &mut App) + 'static, + ) { + self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) + } + + /// Get the current scroll offset, in terms of the list's items. + pub fn logical_scroll_top(&self) -> ListOffset { + self.0.borrow().logical_scroll_top() + } + + /// Scroll the list by the given offset + pub fn scroll_by(&self, distance: Pixels) { + if distance == px(0.) { + return; + } + + let current_offset = self.logical_scroll_top(); + let state = &mut *self.0.borrow_mut(); + + if distance < px(0.) { + state.follow_state.stop_following(); + } + + let mut cursor = state.items.cursor::(()); + cursor.seek(&Count(current_offset.item_ix), Bias::Right); + + let start_pixel_offset = cursor.start().height + current_offset.offset_in_item; + let new_pixel_offset = (start_pixel_offset + distance).max(px(0.)); + if new_pixel_offset > start_pixel_offset { + cursor.seek_forward(&Height(new_pixel_offset), Bias::Right); + } else { + cursor.seek(&Height(new_pixel_offset), Bias::Right); + } + + let scroll_top = ListOffset { + item_ix: cursor.start().count, + offset_in_item: new_pixel_offset - cursor.start().height, + }; + drop(cursor); + state.rebase_pending_scroll(scroll_top); + state.logical_scroll_top = Some(scroll_top); + } + + /// Scroll the list to the very end (past the last item). + /// + /// Unlike [`scroll_to_reveal_item`], this uses the total item count as the + /// anchor, so the list's layout pass will walk backwards from the end and + /// always show the bottom of the last item — even when that item is still + /// growing (e.g. during streaming). + pub fn scroll_to_end(&self) { + let state = &mut *self.0.borrow_mut(); + let item_count = state.items.summary().count; + state.pending_scroll = None; + state.logical_scroll_top = Some(ListOffset { + item_ix: item_count, + offset_in_item: px(0.), + }); + } + + /// Set the follow mode for the list. In `Tail` mode, the list + /// will auto-scroll to the end and re-engage after the user + /// scrolls back to the bottom. In `Normal` mode, no automatic + /// following occurs. + pub fn set_follow_mode(&self, mode: FollowMode) { + let state = &mut *self.0.borrow_mut(); + + match mode { + FollowMode::Normal => { + state.follow_state = FollowState::Normal; + } + FollowMode::Tail => { + state.follow_state = FollowState::Tail { is_following: true }; + if matches!(mode, FollowMode::Tail) { + let item_count = state.items.summary().count; + state.logical_scroll_top = Some(ListOffset { + item_ix: item_count, + offset_in_item: px(0.), + }); + } + } + } + } + + /// Pause tail-following, freezing the list at its current scroll + /// position. Unlike [`Self::set_follow_mode`] with [`FollowMode::Normal`], + /// this keeps the list in `Tail` mode, so it will resume following + /// automatically once the view returns to the bottom. No-op when the list + /// isn't currently following. + /// + /// Useful when something other than the user grows an item (e.g. zooming a + /// diagram) and the current position should stay put rather than snapping + /// to the end. + pub fn pause_following_tail(&self) { + self.0.borrow_mut().follow_state.stop_following(); + } + + /// Returns whether the list is currently actively following the + /// tail (snapping to the end on each layout). + pub fn is_following_tail(&self) -> bool { + matches!( + self.0.borrow().follow_state, + FollowState::Tail { is_following: true } + ) + } + + /// Scroll the list to the given offset + pub fn scroll_to(&self, mut scroll_top: ListOffset) { + let state = &mut *self.0.borrow_mut(); + let item_count = state.items.summary().count; + if scroll_top.item_ix >= item_count { + scroll_top.item_ix = item_count; + scroll_top.offset_in_item = px(0.); + } + + if scroll_top.item_ix < item_count { + state.follow_state.stop_following(); + } + + state.rebase_pending_scroll(scroll_top); + state.logical_scroll_top = Some(scroll_top); + } + + /// Scroll the list to the given item, such that the item is fully visible. + pub fn scroll_to_reveal_item(&self, ix: usize) { + let state = &mut *self.0.borrow_mut(); + + let mut scroll_top = state.logical_scroll_top(); + let height = state + .last_layout_bounds + .map_or(px(0.), |bounds| bounds.size.height); + let padding = state.last_padding.unwrap_or_default(); + + if ix <= scroll_top.item_ix { + scroll_top.item_ix = ix; + scroll_top.offset_in_item = px(0.); + } else { + let mut cursor = state.items.cursor::(()); + cursor.seek(&Count(ix + 1), Bias::Right); + let bottom = cursor.start().height + padding.top; + let goal_top = px(0.).max(bottom - height + padding.bottom); + + cursor.seek(&Height(goal_top), Bias::Left); + let start_ix = cursor.start().count; + let start_item_top = cursor.start().height; + + if start_ix >= scroll_top.item_ix { + scroll_top.item_ix = start_ix; + scroll_top.offset_in_item = goal_top - start_item_top; + } + } + + state.rebase_pending_scroll(scroll_top); + state.logical_scroll_top = Some(scroll_top); + } + + /// Get the bounds for the given item in window coordinates, if it's + /// been rendered. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + let state = &*self.0.borrow(); + + let bounds = state.last_layout_bounds.unwrap_or_default(); + let scroll_top = state.logical_scroll_top(); + if ix < scroll_top.item_ix { + return None; + } + + let mut cursor = state.items.cursor::>(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + + let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; + + cursor.seek_forward(&Count(ix), Bias::Right); + if let Some(&ListItem::Measured { size, .. }) = cursor.item() { + let &Dimensions(Count(count), Height(top), _) = cursor.start(); + if count == ix { + let top = bounds.top() + top - scroll_top; + return Some(Bounds::from_corners( + point(bounds.left(), top), + point(bounds.right(), top + size.height), + )); + } + } + None + } + + /// Call this method when the user starts dragging the scrollbar. + /// + /// This will prevent the height reported to the scrollbar from changing during the drag + /// as items in the overdraw get measured, and help offset scroll position changes accordingly. + pub fn scrollbar_drag_started(&self) { + let mut state = self.0.borrow_mut(); + state.scrollbar_drag_start_height = Some(state.items.summary().height); + } + + /// Called when the user stops dragging the scrollbar. + /// + /// See `scrollbar_drag_started`. + pub fn scrollbar_drag_ended(&self) { + self.0.borrow_mut().scrollbar_drag_start_height.take(); + } + + /// Returns `true` if the scrollbar is currently being dragged. + /// + /// This is set between [`scrollbar_drag_started`](Self::scrollbar_drag_started) + /// and [`scrollbar_drag_ended`](Self::scrollbar_drag_ended) calls. Useful for + /// consumers that need to distinguish scrollbar drags from wheel/trackpad scrolls, + /// e.g. to suppress auto-scroll behavior during manual positioning. + pub fn is_scrollbar_dragging(&self) -> bool { + self.0.borrow().scrollbar_drag_start_height.is_some() + } + + /// Set the offset from the scrollbar + pub fn set_offset_from_scrollbar(&self, point: Point) { + self.0.borrow_mut().set_offset_from_scrollbar(point); + } + + /// Returns the maximum scroll offset according to the items we have measured. + /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly. + pub fn max_offset_for_scrollbar(&self) -> Point { + let state = self.0.borrow(); + point(Pixels::ZERO, state.max_scroll_offset()) + } + + /// Returns the current scroll offset adjusted for the scrollbar. + /// + /// The returned offset has a negative `y` component representing + /// how far the content has scrolled. + pub fn scroll_px_offset_for_scrollbar(&self) -> Point { + let state = &self.0.borrow(); + + if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom { + return Point::new(px(0.), -state.max_scroll_offset()); + } + + let logical_scroll_top = state.logical_scroll_top(); + + let mut cursor = state.items.cursor::(()); + let summary: ListItemSummary = + cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right); + let offset = summary.height + logical_scroll_top.offset_in_item; + + Point::new(px(0.), -offset) + } + + /// Return the bounds of the viewport in pixels. + pub fn viewport_bounds(&self) -> Bounds { + self.0.borrow().last_layout_bounds.unwrap_or_default() + } + + /// Returns whether the item is entirely above the viewport, or `None` if + /// the list has not measured enough layout to know. + /// + /// A zero-height viewport still yields a definitive answer: callers may + /// size sibling UI based on this query (potentially squeezing the list + /// itself to zero height), so returning `None` in that case would make + /// the answer oscillate from frame to frame. + pub fn item_is_above_viewport(&self, ix: usize) -> Option { + let viewport_bounds = self.0.borrow().last_layout_bounds?; + + let scroll_top = self.logical_scroll_top(); + if ix < scroll_top.item_ix { + // Rows before the logical scroll top have no item bounds, but + // their position relative to the viewport is known from scroll state. + return Some(true); + } + + let item_bounds = self.bounds_for_item(ix)?; + Some(item_bounds.bottom() <= viewport_bounds.top()) + } + + /// Returns whether the item is entirely below the viewport, or `None` if + /// the list has not measured enough layout to know. + /// + /// See [`Self::item_is_above_viewport`] for why a zero-height viewport + /// still yields a definitive answer. + pub fn item_is_below_viewport(&self, ix: usize) -> Option { + let viewport_bounds = self.0.borrow().last_layout_bounds?; + + let scroll_top = self.logical_scroll_top(); + if ix < scroll_top.item_ix { + // Rows before the logical scroll top have no item bounds, but + // their position relative to the viewport is known from scroll state. + return Some(false); + } + + let item_bounds = self.bounds_for_item(ix)?; + Some(item_bounds.top() >= viewport_bounds.bottom()) + } +} + +impl StateInner { + /// Re-anchor a pending scroll adjustment from a remeasure onto a newly set + /// scroll position, so it clamps to the remeasured item's new height on + /// the next layout instead of reverting the scroll. + fn rebase_pending_scroll(&mut self, scroll_top: ListOffset) { + let Some(pending) = self.pending_scroll.take() else { + return; + }; + if scroll_top.item_ix >= self.items.summary().count { + return; + } + + self.pending_scroll = match pending { + PendingScroll::Absolute { .. } => Some(PendingScroll::Absolute { + item_ix: scroll_top.item_ix, + offset: scroll_top.offset_in_item, + }), + PendingScroll::Proportional(_) => { + let mut cursor = self.items.cursor::(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + cursor + .item() + .and_then(|item| item.size_hint()) + .filter(|size| size.height.0 > 0.0) + .map(|size| { + PendingScroll::Proportional(PendingScrollFraction { + item_ix: scroll_top.item_ix, + fraction: (scroll_top.offset_in_item.0 / size.height.0).clamp(0.0, 1.0), + }) + }) + } + }; + } + + fn max_scroll_offset(&self) -> Pixels { + let bounds = self.last_layout_bounds.unwrap_or_default(); + let height = self + .scrollbar_drag_start_height + .unwrap_or_else(|| self.items.summary().height); + (height - bounds.size.height).max(px(0.)) + } + + fn visible_range( + items: &SumTree, + height: Pixels, + scroll_top: &ListOffset, + ) -> Range { + let mut cursor = items.cursor::(()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + let start_y = cursor.start().height + scroll_top.offset_in_item; + cursor.seek_forward(&Height(start_y + height), Bias::Left); + scroll_top.item_ix..cursor.start().count + 1 + } + + fn scroll( + &mut self, + scroll_top: &ListOffset, + height: Pixels, + delta: Point, + current_view: EntityId, + window: &mut Window, + cx: &mut App, + ) { + // Drop scroll events after a reset, since we can't calculate + // the new logical scroll top without the item heights + if self.reset { + return; + } + + let padding = self.last_padding.unwrap_or_default(); + let scroll_max = + (self.items.summary().height + padding.top + padding.bottom - height).max(px(0.)); + let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) + .max(px(0.)) + .min(scroll_max); + + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.pending_scroll = None; + self.logical_scroll_top = None; + } else { + let (start, ..) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + let scroll_top = ListOffset { + item_ix: start.count, + offset_in_item: new_scroll_top - start.height, + }; + // The user's scroll supersedes the position stashed by a + // remeasure; re-anchor the pending adjustment so it doesn't revert + // this scroll on the next layout. + self.rebase_pending_scroll(scroll_top); + self.logical_scroll_top = Some(scroll_top); + } + + if delta.y > px(0.) { + self.follow_state.stop_following(); + } + + if let Some(handler) = self.scroll_handler.as_mut() { + let visible_range = Self::visible_range(&self.items, height, scroll_top); + handler( + &ListScrollEvent { + visible_range, + count: self.items.summary().count, + is_scrolled: self.logical_scroll_top.is_some(), + is_following_tail: matches!( + self.follow_state, + FollowState::Tail { is_following: true } + ), + }, + window, + cx, + ); + } + + cx.notify(current_view); + } + + fn logical_scroll_top(&self) -> ListOffset { + self.logical_scroll_top + .unwrap_or_else(|| match self.alignment { + ListAlignment::Top => ListOffset { + item_ix: 0, + offset_in_item: px(0.), + }, + ListAlignment::Bottom => ListOffset { + item_ix: self.items.summary().count, + offset_in_item: px(0.), + }, + }) + } + + fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { + let (start, ..) = self.items.find::( + (), + &Count(logical_scroll_top.item_ix), + Bias::Right, + ); + start.height + logical_scroll_top.offset_in_item + } + + fn layout_all_items( + &mut self, + available_width: Pixels, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) { + match &mut self.measuring_behavior { + ListMeasuringBehavior::Visible => { + return; + } + ListMeasuringBehavior::Measure(has_measured) => { + if *has_measured { + return; + } + *has_measured = true; + } + } + + let mut cursor = self.items.cursor::(()); + let available_item_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::MinContent, + ); + + let mut measured_items = Vec::default(); + + for (ix, item) in cursor.enumerate() { + let size = item.size().unwrap_or_else(|| { + let mut element = render_item(ix, window, cx); + element.layout_as_root(available_item_space, window, cx) + }); + + measured_items.push(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } + + self.items = SumTree::from_iter(measured_items, ()); + } + + fn layout_items( + &mut self, + available_width: Option, + available_height: Pixels, + padding: &Edges, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) -> LayoutItemsResponse { + let old_items = self.items.clone(); + let mut measured_items = VecDeque::new(); + let mut item_layouts = VecDeque::new(); + let mut rendered_height = padding.top; + let mut max_item_width = px(0.); + let mut scroll_top = self.logical_scroll_top(); + + if self.follow_state.is_following() { + scroll_top = ListOffset { + item_ix: self.items.summary().count, + offset_in_item: px(0.), + }; + self.logical_scroll_top = Some(scroll_top); + } + + let mut rendered_focused_item = false; + + let available_item_space = size( + available_width.map_or(AvailableSpace::MaxContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + + let mut cursor = old_items.cursor::(()); + + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= available_height + self.overdraw { + break; + } + + // Use the previously cached height and focus handle if available + let mut size = item.size(); + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < available_height || size.is_none() { + let item_index = scroll_top.item_ix + ix; + let mut element = render_item(item_index, window, cx); + let element_size = element.layout_as_root(available_item_space, window, cx); + size = Some(element_size); + + // If there's a pending scroll adjustment for the scroll-top + // item, apply it. + if ix == 0 { + if let Some(pending_scroll) = self.pending_scroll.take() { + match pending_scroll { + PendingScroll::Absolute { item_ix, offset } + if item_ix == scroll_top.item_ix => + { + scroll_top.offset_in_item = offset.min(element_size.height); + self.logical_scroll_top = Some(scroll_top); + } + PendingScroll::Proportional(pending_scroll) + if pending_scroll.item_ix == scroll_top.item_ix => + { + // Ensuring proportional scroll position is + // maintained after re-measuring. + scroll_top.offset_in_item = + Pixels(pending_scroll.fraction * element_size.height.0); + self.logical_scroll_top = Some(scroll_top); + } + _ => {} + } + } + } + + if visible_height < available_height { + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(window, cx) { + rendered_focused_item = true; + } + } + } + + let size = size.unwrap(); + rendered_height += size.height; + max_item_width = max_item_width.max(size.width); + measured_items.push_back(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } + rendered_height += padding.bottom; + + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < available_height { + while rendered_height < available_height { + cursor.prev(); + if let Some(item) = cursor.item() { + let item_index = cursor.start().0; + let mut element = render_item(item_index, window, cx); + let element_size = element.layout_as_root(available_item_space, window, cx); + let focus_handle = item.focus_handle(); + rendered_height += element_size.height; + measured_items.push_front(ListItem::Measured { + size: element_size, + focus_handle, + }); + item_layouts.push_front(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(window, cx) { + rendered_focused_item = true; + } + } else { + break; + } + } + + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + + match self.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + self.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - available_height, + }; + self.logical_scroll_top = None; + } + }; + } + + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < self.overdraw { + cursor.prev(); + if let Some(item) = cursor.item() { + let size = if let ListItem::Measured { size, .. } = item { + *size + } else { + let mut element = render_item(cursor.start().0, window, cx); + element.layout_as_root(available_item_space, window, cx) + }; + + leading_overdraw += size.height; + measured_items.push_front(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); + } else { + break; + } + } + + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(()); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right); + new_items.extend(measured_items, ()); + cursor.seek(&Count(measured_range.end), Bias::Right); + new_items.append(cursor.suffix(), ()); + self.items = new_items; + + // If follow_tail mode is on but the user scrolled away + // (is_following is false), check whether the current scroll + // position has returned to the bottom. + if self.follow_state.has_stopped_following() { + let padding = self.last_padding.unwrap_or_default(); + let total_height = self.items.summary().height + padding.top + padding.bottom; + let scroll_offset = self.scroll_top(&scroll_top); + if scroll_offset + available_height >= total_height - px(1.0) { + self.follow_state.start_following(); + } + } + + // If none of the visible items are focused, check if an off-screen item is focused + // and include it to be rendered after the visible items so keyboard interaction continues + // to work for it. + if !rendered_focused_item { + let mut cursor = self + .items + .filter::<_, Count>((), |summary| summary.has_focus_handles); + cursor.next(); + while let Some(item) = cursor.item() { + if item.contains_focused(window, cx) { + let item_index = cursor.start().0; + let mut element = render_item(cursor.start().0, window, cx); + let size = element.layout_as_root(available_item_space, window, cx); + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size, + }); + break; + } + cursor.next(); + } + } + + LayoutItemsResponse { + max_item_width, + scroll_top, + item_layouts, + } + } + + fn prepaint_items( + &mut self, + bounds: Bounds, + padding: Edges, + autoscroll: bool, + render_item: &mut RenderItemFn, + window: &mut Window, + cx: &mut App, + ) -> Result { + window.transact(|window| { + match self.measuring_behavior { + ListMeasuringBehavior::Measure(has_measured) if !has_measured => { + self.layout_all_items(bounds.size.width, render_item, window, cx); + } + _ => {} + } + + let mut layout_response = self.layout_items( + Some(bounds.size.width), + bounds.size.height, + &padding, + render_item, + window, + cx, + ); + + // Avoid honoring autoscroll requests from elements other than our children. + window.take_autoscroll(); + + // Only paint the visible items, if there is actually any space for them (taking padding into account) + if bounds.size.height > padding.top + padding.bottom { + let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); + item_origin.y -= layout_response.scroll_top.offset_in_item; + for item in &mut layout_response.item_layouts { + window.with_content_mask(Some(ContentMask { bounds }), |window| { + item.element.prepaint_at(item_origin, window, cx); + }); + + if let Some(autoscroll_bounds) = window.take_autoscroll() + && autoscroll + { + if autoscroll_bounds.top() < bounds.top() { + let mut item_ix = item.index; + let mut offset_in_item = autoscroll_bounds.top() - item_origin.y; + + // The requested top can sit above this item's own + // top. Walk into earlier items so the offset stays + // non-negative and no blank space appears above the + // list. + if offset_in_item < Pixels::ZERO { + let mut cursor = self.items.cursor::(()); + cursor.seek(&Count(item_ix), Bias::Right); + while offset_in_item < Pixels::ZERO { + cursor.prev(); + let Some(prev_item) = cursor.item() else { + offset_in_item = Pixels::ZERO; + break; + }; + let size = prev_item.size().unwrap_or_else(|| { + let mut element = render_item(cursor.start().0, window, cx); + let item_available_size = size( + bounds.size.width.into(), + AvailableSpace::MinContent, + ); + element.layout_as_root(item_available_size, window, cx) + }); + item_ix = cursor.start().0; + offset_in_item += size.height; + } + } + + return Err(ListOffset { + item_ix, + offset_in_item, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::(()); + cursor.seek(&Count(item.index), Bias::Right); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = render_item(cursor.start().0, window, cx); + let item_available_size = + size(bounds.size.width.into(), AvailableSpace::MinContent); + item.layout_as_root(item_available_size, window, cx) + }); + height -= size.height; + } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); + } + } + + item_origin.y += item.size.height; + } + } else { + layout_response.item_layouts.clear(); + } + + Ok(layout_response) + }) + } + + // Scrollbar support + + fn set_offset_from_scrollbar(&mut self, point: Point) { + let Some(bounds) = self.last_layout_bounds else { + return; + }; + let height = bounds.size.height; + + let padding = self.last_padding.unwrap_or_default(); + // Scrollbar drag positions are computed from the content height + // captured at drag start, so map them back using the same height. + let content_height = self + .scrollbar_drag_start_height + .unwrap_or_else(|| self.items.summary().height); + let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.)); + let new_scroll_top = (-point.y).max(px(0.)).min(scroll_max); + + // If content grew during the drag, the frozen bottom is below the + // live bottom. Treat dragging to the frozen end as resuming tail follow. + let dragged_to_end = + scroll_max > px(0.) && new_scroll_top >= (scroll_max - px(1.0)).max(px(0.)); + if dragged_to_end && matches!(self.follow_state, FollowState::Tail { .. }) { + self.follow_state = FollowState::Tail { is_following: true }; + let item_count = self.items.summary().count; + self.pending_scroll = None; + self.logical_scroll_top = Some(ListOffset { + item_ix: item_count, + offset_in_item: px(0.), + }); + return; + } + + self.follow_state.stop_following(); + + if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { + self.pending_scroll = None; + self.logical_scroll_top = None; + } else { + let (start, _, _) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + + let scroll_top = ListOffset { + item_ix: start.count, + offset_in_item: new_scroll_top - start.height, + }; + self.rebase_pending_scroll(scroll_top); + self.logical_scroll_top = Some(scroll_top); + } + } +} + +impl std::fmt::Debug for ListItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unmeasured { .. } => write!(f, "Unrendered"), + Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), + } + } +} + +/// An offset into the list's items, in terms of the item index and the number +/// of pixels off the top left of the item. +#[derive(Debug, Clone, Copy, Default)] +pub struct ListOffset { + /// The index of an item in the list + pub item_ix: usize, + /// The number of pixels to offset from the item index. + pub offset_in_item: Pixels, +} + +impl Element for List { + type RequestLayoutState = (); + type PrepaintState = ListPrepaintState; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (crate::LayoutId, Self::RequestLayoutState) { + let layout_id = match self.sizing_behavior { + ListSizingBehavior::Infer => { + let mut style = Style::default(); + style.overflow.y = Overflow::Scroll; + style.refine(&self.style); + window.with_text_style(style.text_style().cloned(), |window| { + let state = &mut *self.state.0.borrow_mut(); + + let available_height = if let Some(last_bounds) = state.last_layout_bounds { + last_bounds.size.height + } else { + // If we don't have the last layout bounds (first render), + // we might just use the overdraw value as the available height to layout enough items. + state.overdraw + }; + let padding = style.padding.to_pixels( + state.last_layout_bounds.unwrap_or_default().size.into(), + window.rem_size(), + ); + + let layout_response = state.layout_items( + None, + available_height, + &padding, + &mut self.render_item, + window, + cx, + ); + let max_element_width = layout_response.max_item_width; + + let summary = state.items.summary(); + let total_height = summary.height; + + window.request_measured_layout( + style, + move |known_dimensions, available_space, _window, _cx| { + let width = + known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + max_element_width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => total_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + total_height + } + }; + size(width, height) + }, + ) + }) + } + ListSizingBehavior::Auto => { + let mut style = Style::default(); + style.refine(&self.style); + window.with_text_style(style.text_style().cloned(), |window| { + window.request_layout(style, None, cx) + }) + } + }; + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> ListPrepaintState { + let state = &mut *self.state.0.borrow_mut(); + state.reset = false; + + let mut style = Style::default(); + style.refine(&self.style); + + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + + // If the width of the list has changed, invalidate all cached item heights + if state + .last_layout_bounds + .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) + { + let new_items = SumTree::from_iter( + state.items.iter().map(|item| ListItem::Unmeasured { + size_hint: None, + focus_handle: item.focus_handle(), + }), + (), + ); + + state.items = new_items; + state.measuring_behavior.reset(); + } + + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + let layout = + match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) + .unwrap() + } + }; + + state.last_layout_bounds = Some(bounds); + state.last_padding = Some(padding); + ListPrepaintState { hitbox, layout } + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let current_view = window.current_view(); + + // Register the scroll listener before painting children so that, in + // the bubble phase (which runs in reverse registration order), + // children's scroll-wheel handlers run first and can stop propagation + // to prevent the list from scrolling. This matches the ordering of + // div-based scroll containers. + let list_state = self.state.clone(); + let height = bounds.size.height; + let scroll_top = prepaint.layout.scroll_top; + let hitbox_id = prepaint.hitbox.id; + let mut accumulated_scroll_delta = ScrollDelta::default(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) { + accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta); + let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.)); + list_state.0.borrow_mut().scroll( + &scroll_top, + height, + pixel_delta, + current_view, + window, + cx, + ) + } + }); + + window.with_content_mask(Some(ContentMask { bounds }), |window| { + for item in &mut prepaint.layout.item_layouts { + item.element.paint(window, cx); + } + }); + } +} + +impl IntoElement for List { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for List { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl sum_tree::Item for ListItem { + type Summary = ListItemSummary; + + fn summary(&self, _: ()) -> Self::Summary { + match self { + ListItem::Unmeasured { + size_hint, + focus_handle, + } => ListItemSummary { + count: 1, + rendered_count: 0, + unrendered_count: 1, + height: if let Some(size) = size_hint { + size.height + } else { + px(0.) + }, + has_focus_handles: focus_handle.is_some(), + has_unknown_height: size_hint.is_none(), + }, + ListItem::Measured { + size, focus_handle, .. + } => ListItemSummary { + count: 1, + rendered_count: 1, + unrendered_count: 0, + height: size.height, + has_focus_handles: focus_handle.is_some(), + has_unknown_height: false, + }, + } + } +} + +impl sum_tree::ContextLessSummary for ListItemSummary { + fn zero() -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &Self) { + self.count += summary.count; + self.rendered_count += summary.rendered_count; + self.unrendered_count += summary.unrendered_count; + self.height += summary.height; + self.has_focus_handles |= summary.has_focus_handles; + self.has_unknown_height |= summary.has_unknown_height; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { + self.0 += summary.count; + } +} + +impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { + fn zero(_cx: ()) -> Self { + Default::default() + } + + fn add_summary(&mut self, summary: &'a ListItemSummary, _: ()) { + self.0 += summary.height; + } +} + +impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Count { + fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.count).unwrap() + } +} + +impl sum_tree::SeekTarget<'_, ListItemSummary, ListItemSummary> for Height { + fn cmp(&self, other: &ListItemSummary, _: ()) -> std::cmp::Ordering { + self.0.partial_cmp(&other.height).unwrap() + } +} + +#[cfg(test)] +mod test { + + use gpui::{ScrollDelta, ScrollWheelEvent}; + use std::cell::Cell; + use std::rc::Rc; + + use crate::{ + self as gpui, AppContext, Bounds, Context, Element, FollowMode, InteractiveElement, + IntoElement, ListState, Render, Styled, TestAppContext, Window, canvas, div, list, point, + px, size, + }; + + #[gpui::test] + fn test_autoscroll_above_item_top_renders_items_above(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(0.), + }); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |ix, _, _| { + if ix == 2 { + // Request an autoscroll whose top sits 30px above item 2's + // own top, mimicking a scroll-margin overshoot. + canvas( + |bounds, window, _| { + window.request_autoscroll(Bounds::from_corners( + point(bounds.left(), bounds.top() - px(30.)), + point(bounds.right(), bounds.top() + px(5.)), + )); + }, + |_, _, _, _| {}, + ) + .h(px(20.)) + .w_full() + .into_any() + } else { + div().h(px(20.)).w_full().into_any() + } + }) + .w_full() + .h_full() + } + } + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(60.)), |_, cx| { + cx.new(|_| TestView(state.clone())).into_any_element() + }); + + // 30px above item 2's top, with 20px items, lands 10px into item 0. + let scroll_top = state.logical_scroll_top(); + assert!( + scroll_top.offset_in_item >= px(0.), + "offset_in_item must never be negative (would leave blank space above), got {:?}", + scroll_top.offset_in_item, + ); + assert_eq!(scroll_top.item_ix, 0); + assert_eq!(scroll_top.offset_in_item, px(10.)); + } + + #[gpui::test] + fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + + // Ensure that the list is scrolled to the top + state.scroll_to(gpui::ListOffset { + item_ix: 0, + offset_in_item: px(0.0), + }); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(10.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestView(state.clone())).into_any_element() + }); + + // Reset + state.reset(5); + + // And then receive a scroll event _before_ the next paint + cx.simulate_event(ScrollWheelEvent { + position: point(px(1.), px(1.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), + ..Default::default() + }); + + // Scroll position should stay at the top of the list + assert_eq!(state.logical_scroll_top().item_ix, 0); + assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); + } + + #[gpui::test] + fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { + cx.new(|_| TestView(state.clone())).into_any_element() + }); + + // Test positive distance: start at item 1, move down 30px + state.scroll_by(px(30.)); + + // Should move to item 2 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 1); + assert_eq!(offset.offset_in_item, px(10.)); + + // Test negative distance: start at item 2, move up 30px + state.scroll_by(px(-30.)); + + // Should move back to item 1 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + + // Test zero distance + state.scroll_by(px(0.)); + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + } + + #[gpui::test] + fn test_child_scroll_handler_can_stop_list_scroll(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + let child_saw_event = Rc::new(Cell::new(false)); + + struct TestView { + state: ListState, + child_saw_event: Rc>, + } + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let child_saw_event = self.child_saw_event.clone(); + list(self.state.clone(), move |_, _, _| { + let child_saw_event = child_saw_event.clone(); + div() + .h(px(20.)) + .w_full() + .on_scroll_wheel(move |_, _, cx| { + child_saw_event.set(true); + cx.stop_propagation(); + }) + .into_any() + }) + .w_full() + .h_full() + } + } + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { + cx.new(|_| TestView { + state: state.clone(), + child_saw_event: child_saw_event.clone(), + }) + .into_any_element() + }); + + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(10.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), + ..Default::default() + }); + + assert!( + child_saw_event.get(), + "the child's scroll-wheel handler should run" + ); + // The child stopped propagation, so the list must not have scrolled. + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + } + + struct TestListView(ListState); + impl Render for TestListView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + #[gpui::test] + fn test_item_viewport_queries_return_none_before_layout(_cx: &mut TestAppContext) { + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + assert_eq!(state.item_is_above_viewport(0), None); + assert_eq!(state.item_is_below_viewport(0), None); + } + + #[gpui::test] + fn test_item_viewport_queries_before_logical_scroll_top(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(0.), + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(1), Some(true)); + assert_eq!(state.item_is_below_viewport(1), Some(false)); + } + + #[gpui::test] + fn test_item_viewport_queries_measured_item_inside_viewport(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(0.), + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(2), Some(false)); + assert_eq!(state.item_is_below_viewport(2), Some(false)); + } + + #[gpui::test] + fn test_item_viewport_queries_measured_item_above_viewport(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(20.), + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(2), Some(true)); + assert_eq!(state.item_is_below_viewport(2), Some(false)); + } + + #[gpui::test] + fn test_item_viewport_queries_measured_item_below_viewport(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(0.), + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(3), Some(false)); + assert_eq!(state.item_is_below_viewport(3), Some(true)); + } + + #[gpui::test] + fn test_item_viewport_queries_remain_stable_with_zero_height_viewport(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(0.), + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(3), Some(false)); + assert_eq!(state.item_is_below_viewport(3), Some(true)); + + // Squeeze the list to zero height, e.g. because a sibling element + // (sized based on the queries above) consumed all the space. The + // answers must remain definitive rather than becoming `None`, + // otherwise the sibling's size can oscillate between frames. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(0.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + assert_eq!(state.item_is_above_viewport(1), Some(true)); + assert_eq!(state.item_is_below_viewport(1), Some(false)); + assert_eq!(state.item_is_above_viewport(3), Some(false)); + assert_eq!(state.item_is_below_viewport(3), Some(true)); + } + + #[gpui::test] + fn test_item_viewport_queries_after_scroll_to_end_before_layout(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.)).measure_all(); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_, cx| { + cx.new(|_| TestListView(state.clone())).into_any_element() + }); + + state.scroll_to_end(); + + assert_eq!(state.logical_scroll_top().item_ix, state.item_count()); + assert_eq!(state.item_is_above_viewport(0), Some(true)); + assert_eq!(state.item_is_below_viewport(0), Some(false)); + } + + #[gpui::test] + fn test_measure_all_after_width_change(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + // First draw at width 100: all 10 items measured (total 500px). + // Viewport is 200px, so max scroll offset should be 300px. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); + + // Second draw at a different width: items get invalidated. + // Without the fix, max_offset would drop because unmeasured items + // contribute 0 height. + cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| { + view.into_any_element() + }); + assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); + } + + #[gpui::test] + fn test_remeasure(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // Create a list with 10 items, each 100px tall. We'll keep a reference + // to the item height so we can later change the height and assert how + // `ListState` handles it. + let item_height = Rc::new(Cell::new(100usize)); + let state = ListState::new(10, crate::ListAlignment::Top, px(10.)); + + struct TestView { + state: ListState, + item_height: Rc>, + } + + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let height = self.item_height.get(); + list(self.state.clone(), move |_, _, _| { + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let state_clone = state.clone(); + let item_height_clone = item_height.clone(); + let view = cx.update(|_, cx| { + cx.new(|_| TestView { + state: state_clone, + item_height: item_height_clone, + }) + }); + + // Simulate scrolling 40px inside the element with index 2. Since the + // original item height is 100px, this equates to 40% inside the item. + state.scroll_to(gpui::ListOffset { + item_ix: 2, + offset_in_item: px(40.), + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 2); + assert_eq!(offset.offset_in_item, px(40.)); + + // Update the `item_height` to be 50px instead of 100px so we can assert + // that the scroll position is proportionally preserved, that is, + // instead of 40px from the top of item 2, it should be 20px, since the + // item's height has been halved. + item_height.set(50); + state.remeasure(); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 2); + assert_eq!(offset.offset_in_item, px(20.)); + } + + #[gpui::test] + fn test_remeasure_item_preserves_scroll_offset(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let item_height = Rc::new(Cell::new(100usize)); + let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); + + struct TestView { + state: ListState, + item_height: Rc>, + } + + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let height = self.item_height.get(); + list(self.state.clone(), move |index, _, _| { + let height = if index == 5 { height } else { 100 }; + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let state_clone = state.clone(); + let item_height_clone = item_height.clone(); + let view = cx.update(|_, cx| { + cx.new(|_| TestView { + state: state_clone, + item_height: item_height_clone, + }) + }); + + state.scroll_to(gpui::ListOffset { + item_ix: 5, + offset_in_item: px(40.), + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + item_height.set(200); + state.remeasure_items(5..6); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 5); + assert_eq!(offset.offset_in_item, px(40.)); + } + + #[gpui::test] + fn test_remeasure_then_scroll_does_not_revert_scroll_position(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(100.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = { + let state = state.clone(); + cx.update(|_, cx| cx.new(|_| TestView(state))) + }; + + state.scroll_to(gpui::ListOffset { + item_ix: 5, + offset_in_item: px(40.), + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + state.remeasure_items(5..6); + + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), + ..Default::default() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 5); + assert_eq!(offset.offset_in_item, px(70.)); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 5); + assert_eq!( + offset.offset_in_item, + px(70.), + "scrolling after a remeasure should not be reverted by the stale pending scroll" + ); + } + + #[gpui::test] + fn test_scroll_after_remeasure_clamps_to_shrunk_item_height(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let item_height = Rc::new(Cell::new(100usize)); + let state = ListState::new(20, crate::ListAlignment::Top, px(10.)); + + struct TestView { + state: ListState, + item_height: Rc>, + } + + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let height = self.item_height.get(); + list(self.state.clone(), move |index, _, _| { + let height = if index == 5 { height } else { 100 }; + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = { + let state = state.clone(); + let item_height = item_height.clone(); + cx.update(|_, cx| cx.new(|_| TestView { state, item_height })) + }; + + state.scroll_to(gpui::ListOffset { + item_ix: 5, + offset_in_item: px(40.), + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + // Item 5 shrinks from 100px to 50px and is remeasured... + item_height.set(50); + state.remeasure_items(5..6); + + // ...and then the user scrolls down by 30px before the next frame, + // landing at offset 70. + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-30.))), + ..Default::default() + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + // The rebased pending scroll clamps the user's offset to the item's + // new height instead of leaving it pointing past the end of the item. + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 5); + assert_eq!(offset.offset_in_item, px(50.)); + } + + #[gpui::test] + fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items, each 50px tall → 500px total content, 200px viewport. + // With follow-tail on, the list should always show the bottom. + let item_height = Rc::new(Cell::new(50usize)); + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView { + state: ListState, + item_height: Rc>, + } + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let height = self.item_height.get(); + list(self.state.clone(), move |_, _, _| { + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let state_clone = state.clone(); + let item_height_clone = item_height.clone(); + let view = cx.update(|_, cx| { + cx.new(|_| TestView { + state: state_clone, + item_height: item_height_clone, + }) + }); + + state.set_follow_mode(FollowMode::Tail); + + // First paint — items are 50px, total 500px, viewport 200px. + // Follow-tail should anchor to the end. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + // The scroll should be at the bottom: the last visible items fill the + // 200px viewport from the end of 500px of content (offset 300px). + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 6); + assert_eq!(offset.offset_in_item, px(0.)); + assert!(state.is_following_tail()); + + // Simulate items growing (e.g. streaming content makes each item taller). + // 10 items × 80px = 800px total. + item_height.set(80); + state.remeasure(); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + // After growth, follow-tail should have re-anchored to the new end. + // 800px total − 200px viewport = 600px offset → item 7 at offset 40px, + // but follow-tail anchors to item_count (10), and layout walks back to + // fill 200px, landing at item 7 (7 × 80 = 560, 800 − 560 = 240 > 200, + // so item 8: 8 × 80 = 640, 800 − 640 = 160 < 200 → keeps walking → + // item 7: offset = 800 − 200 = 600, item_ix = 600/80 = 7, remainder 40). + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 7); + assert_eq!(offset.offset_in_item, px(40.)); + assert!(state.is_following_tail()); + } + + #[gpui::test] + fn test_pause_following_tail_reengages_when_still_at_bottom(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + state.set_follow_mode(FollowMode::Tail); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + // Pausing while the view is still at the bottom (e.g. a no-op zoom) + // must not strand follow-tail: the next layout has to re-engage so the + // invariant "at the bottom + new content => visible" is preserved. + state.pause_following_tail(); + assert!(!state.is_following_tail()); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + assert!( + state.is_following_tail(), + "pausing while at the bottom must re-engage follow-tail on the next layout" + ); + } + + #[gpui::test] + fn test_pause_following_tail_freezes_off_bottom(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items, 200px viewport. Item height is adjustable to simulate a + // diagram block growing/shrinking on zoom. + let item_height = Rc::new(Cell::new(50usize)); + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView { + state: ListState, + item_height: Rc>, + } + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let height = self.item_height.get(); + list(self.state.clone(), move |_, _, _| { + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| { + cx.new(|_| TestView { + state: state.clone(), + item_height: item_height.clone(), + }) + }); + state.set_follow_mode(FollowMode::Tail); + + // At the bottom: 500px content, 200px viewport → top at item 6. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert_eq!(state.logical_scroll_top().item_ix, 6); + assert!(state.is_following_tail()); + + // Pause, then grow items (a zoom-in that pushes content below the fold). + // The frozen top must stay put rather than snapping to the new end, and + // following stays paused since we're no longer at the bottom. + state.pause_following_tail(); + item_height.set(80); + state.remeasure(); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 6); + assert_eq!(offset.offset_in_item, px(0.)); + assert!( + !state.is_following_tail(), + "a paused list must not re-engage while the frozen top is off the bottom" + ); + + // Shrink back (zoom-out) so the frozen top once again reaches the + // bottom: follow-tail must re-engage on its own. + item_height.set(50); + state.remeasure(); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + assert!( + state.is_following_tail(), + "returning to the bottom must restore follow-tail" + ); + } + + #[gpui::test] + fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + state.set_follow_mode(FollowMode::Tail); + + // Paint with follow-tail — scroll anchored to the bottom. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| { + cx.new(|_| TestView(state.clone())).into_any_element() + }); + assert!(state.is_following_tail()); + + // Simulate the user scrolling up. + // This should disengage follow-tail. + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(100.))), + ..Default::default() + }); + + assert!( + !state.is_following_tail(), + "follow-tail should disengage when the user scrolls toward the start" + ); + } + + #[gpui::test] + fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + state.set_follow_mode(FollowMode::Tail); + + // Paint with follow-tail — scroll anchored to the bottom. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + // Simulate the scrollbar moving the viewport to the middle. + state.set_offset_from_scrollbar(point(px(0.), px(-150.))); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 3); + assert_eq!(offset.offset_in_item, px(0.)); + assert!( + !state.is_following_tail(), + "follow-tail should disengage when the scrollbar manually repositions the list" + ); + + // A subsequent draw should preserve the user's manual position instead + // of snapping back to the end. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 3); + assert_eq!(offset.offset_in_item, px(0.)); + } + + #[gpui::test] + fn test_scrollbar_drag_with_growing_content(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + let last_item_height = Rc::new(Cell::new(50usize)); + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); + + struct TestView { + state: ListState, + last_item_height: Rc>, + } + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let last_item_height = self.last_item_height.clone(); + list(self.state.clone(), move |index, _, _| { + let height = if index == 9 { + last_item_height.get() + } else { + 50 + }; + div().h(px(height as f32)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| { + cx.new(|_| TestView { + state: state.clone(), + last_item_height: last_item_height.clone(), + }) + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + state.scrollbar_drag_started(); + + state.set_offset_from_scrollbar(point(px(0.), px(-150.))); + let scrollbar_offset_before_growth = state.scroll_px_offset_for_scrollbar(); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 3); + assert_eq!(offset.offset_in_item, px(0.)); + + last_item_height.set(550); + state.remeasure_items(9..10); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + assert_eq!(state.max_offset_for_scrollbar().y, px(300.)); + assert_eq!( + state.scroll_px_offset_for_scrollbar(), + scrollbar_offset_before_growth + ); + + state.set_offset_from_scrollbar(point(px(0.), px(-150.))); + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 3); + assert_eq!(offset.offset_in_item, px(0.)); + } + + #[gpui::test] + fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + // Scroll to the middle of the list (item 3). + state.scroll_to(gpui::ListOffset { + item_ix: 3, + offset_in_item: px(0.), + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 3); + assert_eq!(offset.offset_in_item, px(0.)); + assert!(!state.is_following_tail()); + + // Enable follow-tail — this should immediately snap the scroll anchor + // to the end, like the user just sent a prompt. + state.set_follow_mode(FollowMode::Tail); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + // After paint, scroll should be at the bottom. + // 500px total − 200px viewport = 300px offset → item 6, offset 0. + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 6); + assert_eq!(offset.offset_in_item, px(0.)); + assert!(state.is_following_tail()); + } + + #[gpui::test] + fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + const ITEMS: usize = 10; + const ITEM_SIZE: f32 = 50.0; + + let state = ListState::new( + ITEMS, + crate::ListAlignment::Bottom, + px(ITEMS as f32 * ITEM_SIZE), + ); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(ITEM_SIZE)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { + cx.new(|_| TestView(state.clone())).into_any_element() + }); + + // Bottom-aligned lists start pinned to the end: logical_scroll_top returns + // item_ix == item_count, meaning no explicit scroll position has been set. + assert_eq!(state.logical_scroll_top().item_ix, ITEMS); + + let max_offset = state.max_offset_for_scrollbar(); + let scroll_offset = state.scroll_px_offset_for_scrollbar(); + + assert_eq!( + -scroll_offset.y, max_offset.y, + "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom", + -scroll_offset.y, max_offset.y, + ); + } + + /// When the user scrolls away from the bottom during follow_tail, + /// follow_tail suspends. If they scroll back to the bottom, the + /// next paint should re-engage follow_tail using fresh measurements. + #[gpui::test] + fn test_follow_tail_reengages_when_scrolled_back_to_bottom(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + state.set_follow_mode(FollowMode::Tail); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + // Scroll up — follow_tail should suspend (not fully disengage). + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(50.))), + ..Default::default() + }); + assert!(!state.is_following_tail()); + + // Scroll back down to the bottom. + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))), + ..Default::default() + }); + + // After a paint, follow_tail should re-engage because the + // layout confirmed we're at the true bottom. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!( + state.is_following_tail(), + "follow_tail should re-engage after scrolling back to the bottom" + ); + } + + /// When an item is spliced to unmeasured (0px) while follow_tail + /// is suspended, the re-engagement check should still work correctly + #[gpui::test] + fn test_follow_tail_reengagement_not_fooled_by_unmeasured_items(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 20 items × 50px = 1000px total, 200px viewport, 1000px + // overdraw so all items get measured during the follow_tail + // paint (matching realistic production settings). + let state = ListState::new(20, crate::ListAlignment::Top, px(1000.)); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + state.set_follow_mode(FollowMode::Tail); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + // Scroll up a meaningful amount — suspends follow_tail. + // 20 items × 50px = 1000px. viewport 200px. scroll_max = 800px. + // Scrolling up 200px puts us at 600px, clearly not at bottom. + cx.simulate_event(ScrollWheelEvent { + position: point(px(50.), px(100.)), + delta: ScrollDelta::Pixels(point(px(0.), px(200.))), + ..Default::default() + }); + assert!(!state.is_following_tail()); + + // Invalidate the last item (simulates EntryUpdated calling + // remeasure_items). This makes items.summary().height + // temporarily wrong (0px for the invalidated item). + state.remeasure_items(19..20); + + // Paint — layout re-measures the invalidated item with its true + // height. The re-engagement check uses these fresh measurements. + // Since we scrolled 200px up from the 800px max, we're at + // ~600px — NOT at the bottom, so follow_tail should NOT + // re-engage. + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!( + !state.is_following_tail(), + "follow_tail should not falsely re-engage due to an unmeasured item \ + reducing items.summary().height" + ); + } + + #[gpui::test] + fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + + // 10 items × 50px = 500px total, 200px viewport, scroll_max = 300px. + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + state.set_follow_mode(FollowMode::Tail); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + // Drag the scrollbar up to the middle — follow_tail should suspend. + state.set_offset_from_scrollbar(point(px(0.), px(-150.))); + assert!(!state.is_following_tail()); + + // Drag the scrollbar back to the bottom — follow_tail should re-engage + // on the next paint. + state.set_offset_from_scrollbar(point(px(0.), px(-300.))); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + assert!( + state.is_following_tail(), + "follow_tail should re-engage after scrolling back to the bottom via the scrollbar" + ); + } + + #[gpui::test] + fn test_follow_tail_reengages_after_scrollbar_drag_to_bottom_while_growing( + cx: &mut TestAppContext, + ) { + let cx = cx.add_empty_window(); + + let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone(), |_, _, _| { + div().h(px(50.)).w_full().into_any() + }) + .w_full() + .h_full() + } + } + + let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); + + state.set_follow_mode(FollowMode::Tail); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + assert!(state.is_following_tail()); + + state.scrollbar_drag_started(); + + state.splice(10..10, 10); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.clone().into_any_element() + }); + + state.set_offset_from_scrollbar(point(px(0.), px(-300.))); + state.scrollbar_drag_ended(); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { + view.into_any_element() + }); + + assert!( + state.is_following_tail(), + "follow_tail should re-engage when the user drags the scrollbar to \ + the bottom of its track, even when content has grown during the drag \ + (so frozen_bottom < live_bottom)" + ); + } +} diff --git a/vendor/gpui-pre/src/elements/mod.rs b/vendor/gpui-pre/src/elements/mod.rs new file mode 100644 index 0000000..8a2a1b7 --- /dev/null +++ b/vendor/gpui-pre/src/elements/mod.rs @@ -0,0 +1,27 @@ +mod anchored; +mod animation; +mod canvas; +mod container_query; +mod deferred; +mod div; +mod image_cache; +mod img; +mod list; +mod surface; +mod svg; +mod text; +mod uniform_list; + +pub use anchored::*; +pub use animation::*; +pub use canvas::*; +pub use container_query::*; +pub use deferred::*; +pub use div::*; +pub use image_cache::*; +pub use img::*; +pub use list::*; +pub use surface::*; +pub use svg::*; +pub use text::*; +pub use uniform_list::*; diff --git a/vendor/gpui-pre/src/elements/surface.rs b/vendor/gpui-pre/src/elements/surface.rs new file mode 100644 index 0000000..fb07280 --- /dev/null +++ b/vendor/gpui-pre/src/elements/surface.rs @@ -0,0 +1,129 @@ +use crate::{ + App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId, + ObjectFit, Pixels, Style, StyleRefinement, Styled, Window, +}; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use core_video::pixel_buffer::CVPixelBuffer; +use refineable::Refineable; + +/// A source of a surface's content. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SurfaceSource { + /// A CoreVideo image buffer + #[cfg(any(target_os = "macos", target_os = "ios"))] + Surface(CVPixelBuffer), +} + +#[cfg(any(target_os = "macos", target_os = "ios"))] +impl From for SurfaceSource { + fn from(value: CVPixelBuffer) -> Self { + SurfaceSource::Surface(value) + } +} + +/// A surface element. +pub struct Surface { + source: SurfaceSource, + object_fit: ObjectFit, + style: StyleRefinement, +} + +/// Create a new surface element. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub fn surface(source: impl Into) -> Surface { + Surface { + source: source.into(), + object_fit: ObjectFit::Contain, + style: Default::default(), + } +} + +impl Surface { + /// Set the object fit for the image. + pub fn object_fit(mut self, object_fit: ObjectFit) -> Self { + self.object_fit = object_fit; + self + } +} + +impl Element for Surface { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = window.request_layout(style, [], cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + } + + fn paint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + #[cfg_attr( + not(any(target_os = "macos", target_os = "ios")), + allow(unused_variables) + )] + bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + #[cfg_attr( + not(any(target_os = "macos", target_os = "ios")), + allow(unused_variables) + )] + window: &mut Window, + _: &mut App, + ) { + match &self.source { + #[cfg(any(target_os = "macos", target_os = "ios"))] + SurfaceSource::Surface(surface) => { + let size = crate::size(surface.get_width().into(), surface.get_height().into()); + let new_bounds = self.object_fit.get_bounds(bounds, size); + // TODO: Add support for corner_radii + window.paint_surface(new_bounds, surface.clone()); + } + #[allow(unreachable_patterns)] + _ => {} + } + } +} + +impl IntoElement for Surface { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Surface { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} diff --git a/vendor/gpui-pre/src/elements/svg.rs b/vendor/gpui-pre/src/elements/svg.rs new file mode 100644 index 0000000..176072f --- /dev/null +++ b/vendor/gpui-pre/src/elements/svg.rs @@ -0,0 +1,303 @@ +use std::{ + fs, + hash::{Hash, Hasher}, + path::Path, + sync::Arc, +}; + +use crate::{ + App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, + Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size, + StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size, +}; +use gpui_util::ResultExt; + +/// An SVG element. +pub struct Svg { + interactivity: Interactivity, + transformation: Option, + path: Option, + external_path: Option, + data: Option>, + data_path: Option, +} + +/// Create a new SVG element. +#[track_caller] +pub fn svg() -> Svg { + Svg { + interactivity: Interactivity::new(), + transformation: None, + path: None, + external_path: None, + data: None, + data_path: None, + } +} + +impl Svg { + /// Set the path to the SVG file for this element. + pub fn path(mut self, path: impl Into) -> Self { + self.path = Some(path.into()); + self + } + + /// Set the path to the SVG file for this element. + pub fn external_path(mut self, path: impl Into) -> Self { + self.external_path = Some(path.into()); + self + } + + /// Set the raw SVG data for this element. + /// The SVG will be rendered directly from the provided bytes. + pub fn data(mut self, data: &[u8]) -> Self { + // Generate a unique deterministic path based on the data hash for caching + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + data.hash(&mut hasher); + let hash = hasher.finish(); + let path = SharedString::from(format!("__binary_svg__{}", hash)); + self.data = Some(Arc::from(data)); + self.data_path = Some(path); + self + } + + /// Transform the SVG element with the given transformation. + /// Note that this won't effect the hitbox or layout of the element, only the rendering. + pub fn with_transformation(mut self, transformation: Transformation) -> Self { + self.transformation = Some(transformation); + self + } +} + +impl Element for Svg { + type RequestLayoutState = (); + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + self.interactivity.source_location() + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| window.request_layout(style, None, cx), + ); + (layout_id, ()) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + bounds.size, + window, + cx, + |_, _, hitbox, _, _| hitbox, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) where + Self: Sized, + { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |style, window, cx| { + let transformation = self + .transformation + .as_ref() + .map(|transformation| { + transformation.into_matrix(bounds.center(), window.scale_factor()) + }) + .unwrap_or_default(); + + if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) { + if let Some(color) = style.text.color { + window + .paint_svg( + bounds, + path.clone(), + Some(&**data), + transformation, + color, + cx, + ) + .log_err(); + } + } else if let Some((path, color)) = + self.external_path.as_ref().zip(style.text.color) + { + let Some(bytes) = window + .use_asset::(path, cx) + .and_then(|asset| asset.log_err()) + else { + return; + }; + + window + .paint_svg( + bounds, + path.clone(), + Some(&bytes), + transformation, + color, + cx, + ) + .log_err(); + } else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + window + .paint_svg(bounds, path.clone(), None, transformation, color, cx) + .log_err(); + } + }, + ) + } +} + +impl IntoElement for Svg { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Svg { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl InteractiveElement for Svg { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity + } +} + +/// A transformation to apply to an SVG element. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transformation { + scale: Size, + translate: Point, + rotate: Radians, +} + +impl Default for Transformation { + fn default() -> Self { + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } +} + +impl Transformation { + /// Create a new Transformation with the specified scale along each axis. + pub fn scale(scale: Size) -> Self { + Self { + scale, + translate: point(px(0.0), px(0.0)), + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified translation. + pub fn translate(translate: Point) -> Self { + Self { + scale: size(1.0, 1.0), + translate, + rotate: radians(0.0), + } + } + + /// Create a new Transformation with the specified rotation in radians. + pub fn rotate(rotate: impl Into) -> Self { + let rotate = rotate.into(); + Self { + scale: size(1.0, 1.0), + translate: point(px(0.0), px(0.0)), + rotate, + } + } + + /// Update the scaling factor of this transformation. + pub fn with_scaling(mut self, scale: Size) -> Self { + self.scale = scale; + self + } + + /// Update the translation value of this transformation. + pub fn with_translation(mut self, translate: Point) -> Self { + self.translate = translate; + self + } + + /// Update the rotation angle of this transformation. + pub fn with_rotation(mut self, rotate: impl Into) -> Self { + self.rotate = rotate.into(); + self + } + + fn into_matrix(self, center: Point, scale_factor: f32) -> TransformationMatrix { + //Note: if you read this as a sequence of matrix multiplications, start from the bottom + TransformationMatrix::unit() + .translate(center.scale(scale_factor) + self.translate.scale(scale_factor)) + .rotate(self.rotate) + .scale(self.scale) + .translate(center.scale(-scale_factor)) + } +} + +enum SvgAsset {} + +impl Asset for SvgAsset { + type Source = SharedString; + type Output = Result, Arc>; + + fn load( + source: Self::Source, + _cx: &mut App, + ) -> impl Future + Send + 'static { + async move { + let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?; + let bytes = Arc::from(bytes); + Ok(bytes) + } + } +} diff --git a/vendor/gpui-pre/src/elements/text.rs b/vendor/gpui-pre/src/elements/text.rs new file mode 100644 index 0000000..d1896ad --- /dev/null +++ b/vendor/gpui-pre/src/elements/text.rs @@ -0,0 +1,1349 @@ +use crate::{ + ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, + HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, + TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine, + WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window, +}; +use anyhow::Context as _; +use gpui_util::ResultExt; +use itertools::Itertools; +use smallvec::SmallVec; +use std::{ + borrow::Cow, + cell::{Cell, RefCell}, + mem, + ops::{Deref, DerefMut, Range}, + rc::Rc, + sync::Arc, +}; + +/// An [`Element`] that renders text. +/// +/// In general, [`Text`] objects should be created via the [`text`] macro: +/// ```rust +/// # use gpui::*; +/// # fn render() -> impl IntoElement { +/// div().child(text!("hello")) +/// # } +/// ``` +/// ## IDs and Accessibility +/// +/// [`Text`] elements have an ID. This ID is primarily used to produce nodes in +/// the accessibility tree, which allows the text to be visible to screen +/// readers and other assistive technologies. +/// +/// This ID is stable across frames. If the same text, with the same ID, is +/// present in two consecutive frames, no updates are reported to the screen +/// reader. If the text changes, but the ID stays the same, then the screen +/// reader will be notified that a text node's content has changed. **However**, +/// if the ID changes, then the screen reader will be notified that a node has +/// been removed, and a new node has been added. +/// +/// When using the [`text`] macro, each invocation of the macro will get a +/// unique ID, derived from its position in the source code (filename, line, and +/// column). For example: +/// ```rust +/// # use gpui::*; +/// let x = text!("hello"); +/// let y = text!("hello"); +/// // not equal, because different `text!` invocations produced them +/// assert_ne!(x.id(), y.id()); +/// +/// fn make_text(s: &str) -> Text { text!(s) } +/// let x = make_text("hello"); +/// let y = make_text("hello"); +/// // equal, because the same `text!` invocation produced them +/// assert_eq!(x.id(), y.id()); +/// ``` +/// When the contents of an invocation of [`text`] do not change, this +/// distinction is less relevant (with the caveat that you still need to take +/// care to ensure that duplicate IDs do not appear). +/// +/// However, when a [`text`] invocation's argument *does* change, you should +/// consider whether this change should be reported as a node "updating its +/// contents", or an old node being destroyed and a new node being created. +#[derive(Debug, Clone)] +pub struct Text { + id: Option, + text: SharedString, +} + +impl Text { + /// Create a new [`Text`] element with a specific ID. + /// + /// If you want a unique ID to be assigned automatically, use the [`text`] + /// macro. The docs for [`Text`] have more detail about choosing IDs. + #[inline] + pub const fn new(id: ElementId, text: SharedString) -> Self { + Self { id: Some(id), text } + } + + /// Create a new [`Text`] element that is inaccessible to screen readers. + /// + /// In order for text to be accessible to screen readers, it must have an ID + /// provided. If you want text to be accessible, either use [`text`] to have + /// an ID automatically assigned, or use [`Text::new`] to manually assign an + /// ID. + /// + /// This function is intended for use inside custom UI components, where + /// accessible properties may be set on parent containers. + #[inline] + pub const fn new_inaccessible(text: SharedString) -> Self { + Self { id: None, text } + } + + /// The ID of this [`Text`] element. + #[inline] + pub const fn id(&self) -> Option<&ElementId> { + self.id.as_ref() + } + + /// Produce a new [`Text`] with the given `id`. + pub fn with_id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + /// The text that this [`Text`] element will display. + #[inline] + pub const fn text(&self) -> &SharedString { + &self.text + } +} + +impl Deref for Text { + type Target = SharedString; + fn deref(&self) -> &Self::Target { + &self.text + } +} + +impl DerefMut for Text { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.text + } +} + +/// Trivial hash function for the location information produced by the [`text`] +/// macro. Not covered by semver guarantees. Performance is not particularly +/// significant because it's only used on small strings in const contexts. +#[doc(hidden)] +pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 { + const BASIS: u64 = 0xcbf29ce484222325; + const PRIME: u64 = 0x100000001b3; + + let bytes = s.as_bytes(); + let mut hash = BASIS; + let mut i = 0; + while i < bytes.len() { + hash ^= bytes[i] as u64; + hash = hash.wrapping_mul(PRIME); + i += 1; + } + hash +} + +/// Create a new [`Text`] element. +/// +/// ```rust +/// # use gpui::*; +/// let a = text!("hello"); +/// let b = text!(id = "farewell-message", "hello"); +/// +/// ``` +/// +/// Text created with this macro is *accessible*. The macro generates an ID +/// based on the source location. See the docs for [`Text`] for a more in-depth +/// explanation of the significance of the ID of a [`Text`] element. +#[macro_export] +macro_rules! text { + (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }}; + ($text:expr) => {{ + const ID: &'static str = concat!(file!(), "/", line!(), ":", column!()); + const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID); + $crate::Text::new($crate::ElementId::Integer(HASH), $text.into()) + }}; +} + +impl IntoElement for Text { + type Element = Self; + #[inline] + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for Text { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + self.id.clone() + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn a11y_role(&self) -> Option { + if self.id.is_some() { + Some(accesskit::Role::Label) + } else { + None + } + } + + fn write_a11y_info(&self, node: &mut accesskit::Node) { + node.set_value(self.text.to_string()); + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + ::request_layout(&mut self.text, id, inspector_id, window, cx) + } + + fn prepaint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + ::prepaint( + &mut self.text, + id, + inspector_id, + bounds, + request_layout, + window, + cx, + ) + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + ::paint( + &mut self.text, + id, + inspector_id, + bounds, + request_layout, + prepaint, + window, + cx, + ); + } +} + +impl Element for &'static str { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(SharedString::from(*self), None, window, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + text_layout.prepaint(bounds, self) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + text_layout: &mut TextLayout, + _: &mut (), + window: &mut Window, + cx: &mut App, + ) { + text_layout.paint(self, window, cx) + } +} + +impl IntoElement for &'static str { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for String { + type Element = SharedString; + + fn into_element(self) -> Self::Element { + self.into() + } +} + +impl IntoElement for Cow<'static, str> { + type Element = SharedString; + + fn into_element(self) -> Self::Element { + self.into() + } +} + +impl Element for SharedString { + type RequestLayoutState = TextLayout; + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut state = TextLayout::default(); + let layout_id = state.layout(self.clone(), None, window, cx); + (layout_id, state) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + text_layout.prepaint(bounds, self.as_ref()) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + text_layout: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + text_layout.paint(self.as_ref(), window, cx) + } +} + +impl IntoElement for SharedString { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Renders text with runs of different styles. +/// +/// Callers are responsible for setting the correct style for each run. +/// For text with a uniform style, you can usually avoid calling this constructor +/// and just pass text directly. +pub struct StyledText { + text: SharedString, + runs: Option>, + delayed_highlights: Option, HighlightStyle)>>, + delayed_font_family_overrides: Option, SharedString)>>, + layout: TextLayout, +} + +impl StyledText { + /// Construct a new styled text element from the given string. + pub fn new(text: impl Into) -> Self { + StyledText { + text: text.into(), + runs: None, + delayed_highlights: None, + delayed_font_family_overrides: None, + layout: TextLayout::default(), + } + } + + /// Get the layout for this element. This can be used to map indices to pixels and vice versa. + pub fn layout(&self) -> &TextLayout { + &self.layout + } + + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. + pub fn with_default_highlights( + mut self, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + debug_assert!( + self.delayed_highlights.is_none(), + "Can't use `with_default_highlights` and `with_highlights`" + ); + let runs = Self::compute_runs(&self.text, default_style, highlights); + self.with_runs(runs) + } + + /// Set the styling attributes for the given text, as well as + /// as any ranges of text that have had their style customized. + pub fn with_highlights( + mut self, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Self { + debug_assert!( + self.runs.is_none(), + "Can't use `with_highlights` and `with_default_highlights`" + ); + self.delayed_highlights = Some( + highlights + .into_iter() + .inspect(|(run, _)| { + debug_assert!(self.text.is_char_boundary(run.start)); + debug_assert!(self.text.is_char_boundary(run.end)); + }) + .collect::>(), + ); + self + } + + fn compute_runs( + text: &str, + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, + ) -> Vec { + let mut runs = Vec::new(); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + debug_assert!(text.is_char_boundary(range.start)); + runs.push(default_style.clone().to_run(range.start - ix)); + } + debug_assert!(text.is_char_boundary(range.end)); + runs.push( + default_style + .clone() + .highlight(highlight) + .to_run(range.len()), + ); + ix = range.end; + } + if ix < text.len() { + runs.push(default_style.to_run(text.len() - ix)); + } + runs + } + + /// Override the font family for specific byte ranges of the text. + /// + /// This is resolved lazily at layout time, so the overrides are applied + /// on top of the inherited text style from the parent element. + /// Can be combined with [`with_highlights`](Self::with_highlights). + /// + /// The overrides must be sorted by range start and non-overlapping. + /// Each override range must fall on character boundaries. + pub fn with_font_family_overrides( + mut self, + overrides: impl IntoIterator, SharedString)>, + ) -> Self { + self.delayed_font_family_overrides = Some( + overrides + .into_iter() + .inspect(|(range, _)| { + debug_assert!(self.text.is_char_boundary(range.start)); + debug_assert!(self.text.is_char_boundary(range.end)); + }) + .collect(), + ); + self + } + + fn apply_font_family_overrides( + runs: &mut [TextRun], + overrides: &[(Range, SharedString)], + ) { + let mut byte_offset = 0; + let mut override_idx = 0; + for run in runs.iter_mut() { + let run_end = byte_offset + run.len; + while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset { + override_idx += 1; + } + if override_idx < overrides.len() { + let (ref range, ref family) = overrides[override_idx]; + if byte_offset >= range.start && run_end <= range.end { + run.font.family = family.clone(); + } + } + byte_offset = run_end; + } + } + + /// Set the text runs for this piece of text. + pub fn with_runs(mut self, runs: Vec) -> Self { + let mut text = &*self.text; + for run in &runs { + text = text.get(run.len..).unwrap_or_else(|| { + #[cfg(debug_assertions)] + panic!("invalid text run. Text: '{text}', run: {run:?}"); + #[cfg(not(debug_assertions))] + panic!("invalid text run"); + }); + } + assert!(text.is_empty(), "invalid text run"); + self.runs = Some(runs); + self + } +} + +impl Element for StyledText { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let font_family_overrides = self.delayed_font_family_overrides.take(); + let mut runs = self.runs.take().or_else(|| { + self.delayed_highlights.take().map(|delayed_highlights| { + Self::compute_runs(&self.text, &window.text_style(), delayed_highlights) + }) + }); + + if let Some(ref overrides) = font_family_overrides { + let runs = + runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]); + Self::apply_font_family_overrides(runs, overrides); + } + + let layout_id = self.layout.layout(self.text.clone(), runs, window, cx); + (layout_id, ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) { + self.layout.prepaint(bounds, &self.text) + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.layout.paint(&self.text, window, cx) + } +} + +impl IntoElement for StyledText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// The Layout for TextElement. This can be used to map indices to pixels and vice versa. +#[derive(Default, Clone)] +pub struct TextLayout(Rc>>); + +#[derive(Clone, Copy)] +enum TextPaintPass { + All, + Background, + Foreground, +} + +struct TextLayoutInner { + len: usize, + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, + wrap_width: Option, + truncate_width: Option, + size: Option>, + bounds: Option>, +} + +impl TextLayout { + fn layout( + &self, + text: SharedString, + runs: Option>, + window: &mut Window, + _: &mut App, + ) -> LayoutId { + let text_style = window.text_style(); + let font_size = text_style.font_size.to_pixels(window.rem_size()); + let line_height = window.pixel_snap( + text_style + .line_height + .to_pixels(font_size.into(), window.rem_size()), + ); + + let runs = if let Some(runs) = runs { + runs + } else { + vec![text_style.to_run(text.len())] + }; + window.request_measured_layout(Default::default(), { + let element_state = self.clone(); + + move |known_dimensions, available_space, window, cx| { + let wrap_width = if text_style.white_space == WhiteSpace::Normal { + known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => Some(x), + _ => None, + }) + } else { + None + }; + + let (truncate_width, truncation_affix, truncate_from) = + if let Some(text_overflow) = text_style.text_overflow.clone() { + let width = known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => match text_style.line_clamp { + Some(max_lines) => Some(x * max_lines), + None => Some(x), + }, + _ => None, + }); + + match text_overflow { + TextOverflow::Truncate(s) => (width, s, TruncateFrom::End), + TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start), + TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle), + } + } else { + (None, "".into(), TruncateFrom::End) + }; + + // Only use cached layout if: + // 1. We have a cached size + // 2. wrap_width matches (or both are None) + // 3. truncate_width is None (if truncate_width is Some, we need to re-layout + // because the previous layout may have been computed without truncation) + // 4. the cached layout was not truncated (a truncated layout answers an + // unconstrained probe with the truncated size, which poisons intrinsic + // sizing with whatever width some earlier measure pass happened to use) + if let Some(text_layout) = element_state.0.borrow().as_ref() + && let Some(size) = text_layout.size + && (wrap_width.is_none() || wrap_width == text_layout.wrap_width) + && truncate_width.is_none() + && text_layout.truncate_width.is_none() + { + return size; + } + + let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); + let (text, runs) = if let Some(truncate_width) = truncate_width { + if let Some(max_lines) = text_style.line_clamp + && let Some(wrap_width) = wrap_width + { + line_wrapper.truncate_wrapped_line( + text.clone(), + wrap_width, + max_lines, + &truncation_affix, + &runs, + truncate_from, + ) + } else if let Some(unclipped) = window + .text_system() + .shape_text(text.clone(), font_size, &runs, None, None) + .log_err() + && unclipped + .iter() + .all(|line| line.size(line_height).width <= truncate_width) + { + // The truncation decision below sums per-character advances, + // which overestimates the shaped width (no kerning), truncating + // text that fits exactly in its measured width. Skip truncation + // whenever the honestly-shaped text fits; the shaping result + // comes from the line layout cache when the same text was + // already measured untruncated this frame. + (text.clone(), Cow::Borrowed(&*runs)) + } else { + line_wrapper.truncate_line( + text.clone(), + truncate_width, + &truncation_affix, + &runs, + truncate_from, + ) + } + } else { + (text.clone(), Cow::Borrowed(&*runs)) + }; + let len = text.len(); + + let Some(lines) = window + .text_system() + .shape_text( + text, + font_size, + &runs, + wrap_width, // Wrap if we know the width. + text_style.line_clamp, // Limit the number of lines if line_clamp is set. + ) + .log_err() + else { + element_state.0.borrow_mut().replace(TextLayoutInner { + lines: Default::default(), + len: 0, + line_height, + wrap_width, + truncate_width, + size: Some(Size::default()), + bounds: None, + }); + return Size::default(); + }; + + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width).ceil(); + } + + element_state.0.borrow_mut().replace(TextLayoutInner { + lines, + len, + line_height, + wrap_width, + truncate_width, + size: Some(size), + bounds: None, + }); + + size + } + }) + } + + fn prepaint(&self, bounds: Bounds, text: &str) { + let mut element_state = self.0.borrow_mut(); + let element_state = element_state + .as_mut() + .with_context(|| format!("measurement has not been performed on {text}")) + .unwrap(); + element_state.bounds = Some(bounds); + } + + fn paint(&self, text: &str, window: &mut Window, cx: &mut App) { + self.paint_lines(TextPaintPass::All, window, cx) + .with_context(|| format!("failed to paint {text}")) + .unwrap(); + } + + /// Paints the backgrounds of the text runs without the glyphs, so that callers can paint + /// content, such as highlights, between the backgrounds and the text. Follow it with + /// [`Self::paint_foreground`] to paint the glyphs. + pub fn paint_background(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> { + self.paint_lines(TextPaintPass::Background, window, cx) + } + + /// Paints the glyphs and decorations of the text without the run backgrounds. + pub fn paint_foreground(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> { + self.paint_lines(TextPaintPass::Foreground, window, cx) + } + + fn paint_lines( + &self, + pass: TextPaintPass, + window: &mut Window, + cx: &mut App, + ) -> anyhow::Result<()> { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .context("measurement has not been performed")?; + let bounds = element_state + .bounds + .context("prepaint has not been performed")?; + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let text_style = window.text_style(); + for line in &element_state.lines { + if matches!(pass, TextPaintPass::All | TextPaintPass::Background) { + line.paint_background( + line_origin, + line_height, + text_style.text_align, + Some(bounds), + window, + cx, + ) + .log_err(); + } + if matches!(pass, TextPaintPass::All | TextPaintPass::Foreground) { + line.paint( + line_origin, + line_height, + text_style.text_align, + Some(bounds), + window, + cx, + ) + .log_err(); + } + line_origin.y += line.size(line_height).height; + } + Ok(()) + } + + /// Get the byte index into the input of the pixel position. + pub fn index_for_position(&self, mut position: Point) -> Result { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + + if position.y < bounds.top() { + return Err(0); + } + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + for line in &element_state.lines { + let line_bottom = line_origin.y + line.size(line_height).height; + if position.y > line_bottom { + line_origin.y = line_bottom; + line_start_ix += line.len() + 1; + } else { + let position_within_line = position - line_origin; + match line.index_for_position(position_within_line, line_height) { + Ok(index_within_line) => return Ok(line_start_ix + index_within_line), + Err(index_within_line) => return Err(line_start_ix + index_within_line), + } + } + } + + Err(line_start_ix.saturating_sub(1)) + } + + /// Get the pixel position for the given byte index. + pub fn position_for_index(&self, index: usize) -> Option> { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let bounds = element_state + .bounds + .expect("prepaint has not been performed"); + let line_height = element_state.line_height; + + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + + for line in &element_state.lines { + let line_end_ix = line_start_ix + line.len(); + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_origin.y += line.size(line_height).height; + line_start_ix = line_end_ix + 1; + continue; + } else { + let ix_within_line = index - line_start_ix; + return Some(line_origin + line.position_for_index(ix_within_line, line_height)?); + } + } + + None + } + + /// Retrieve the layout for the line containing the given byte index. + pub fn line_layout_for_index(&self, index: usize) -> Option> { + let element_state = self.0.borrow(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + let mut line_start_ix = 0; + + for line in &element_state.lines { + let line_end_ix = line_start_ix + line.len(); + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_start_ix = line_end_ix + 1; + continue; + } else { + return Some(line.layout.clone()); + } + } + + None + } + + /// Retrieve all line layouts in source order. + pub fn line_layouts(&self) -> SmallVec<[Arc; 1]> { + self.0 + .borrow() + .as_ref() + .expect("measurement has not been performed") + .lines + .iter() + .map(|line| line.layout.clone()) + .collect() + } + + /// The bounds of this layout. + pub fn bounds(&self) -> Bounds { + self.0.borrow().as_ref().unwrap().bounds.unwrap() + } + + /// The line height for this layout. + pub fn line_height(&self) -> Pixels { + self.0.borrow().as_ref().unwrap().line_height + } + + /// The UTF-8 length of the underlying text. + pub fn len(&self) -> usize { + self.0.borrow().as_ref().unwrap().len + } + + /// The text for this layout. + pub fn text(&self) -> String { + self.0 + .borrow() + .as_ref() + .unwrap() + .lines + .iter() + .map(|s| &s.text) + .join("\n") + } + + /// The text for this layout (with soft-wraps as newlines) + pub fn wrapped_text(&self) -> String { + let mut accumulator = String::new(); + + for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() { + let mut seen = 0; + for boundary in wrapped.layout.wrap_boundaries.iter() { + let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs + [boundary.glyph_ix] + .index; + + accumulator.push_str(&wrapped.text[seen..index]); + accumulator.push('\n'); + seen = index; + } + accumulator.push_str(&wrapped.text[seen..]); + accumulator.push('\n'); + } + // Remove trailing newline + accumulator.pop(); + accumulator + } +} + +/// A text element that can be interacted with. +pub struct InteractiveText { + element_id: ElementId, + text: StyledText, + click_listener: + Option], InteractiveTextClickEvent, &mut Window, &mut App)>>, + hover_listener: Option, MouseMoveEvent, &mut Window, &mut App)>>, + tooltip_builder: Option Option>>, + tooltip_id: Option, + clickable_ranges: Vec>, +} + +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, +} + +#[doc(hidden)] +#[derive(Default)] +pub struct InteractiveTextState { + mouse_down_index: Rc>>, + hovered_index: Rc>>, + active_tooltip: Rc>>, + long_press_tooltip_active: Rc>, +} + +/// InteractiveTest is a wrapper around StyledText that adds mouse interactions. +impl InteractiveText { + /// Creates a new InteractiveText from the given text. + pub fn new(id: impl Into, text: StyledText) -> Self { + Self { + element_id: id.into(), + text, + click_listener: None, + hover_listener: None, + tooltip_builder: None, + tooltip_id: None, + clickable_ranges: Vec::new(), + } + } + + /// on_click is called when the user clicks on one of the given ranges, passing the index of + /// the clicked range. + pub fn on_click( + mut self, + ranges: Vec>, + listener: impl Fn(usize, &mut Window, &mut App) + 'static, + ) -> Self { + self.click_listener = Some(Box::new(move |ranges, event, window, cx| { + for (range_ix, range) in ranges.iter().enumerate() { + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) + { + listener(range_ix, window, cx); + } + } + })); + self.clickable_ranges = ranges; + self + } + + /// on_hover is called when the mouse moves over a character within the text, passing the + /// index of the hovered character, or None if the mouse leaves the text. + pub fn on_hover( + mut self, + listener: impl Fn(Option, MouseMoveEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.hover_listener = Some(Box::new(listener)); + self + } + + /// tooltip lets you specify a tooltip for a given character index in the string. + pub fn tooltip( + mut self, + builder: impl Fn(usize, &mut Window, &mut App) -> Option + 'static, + ) -> Self { + self.tooltip_builder = Some(Rc::new(builder)); + self + } +} + +impl Element for InteractiveText { + type RequestLayoutState = (); + type PrepaintState = Hitbox; + + fn id(&self) -> Option { + Some(self.element_id.clone()) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn a11y_role(&self) -> Option { + Some(accesskit::Role::Label) + } + + fn write_a11y_info(&self, node: &mut accesskit::Node) { + node.set_value(self.text.text.to_string()); + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + self.text.request_layout(None, inspector_id, window, cx) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Hitbox { + window.with_optional_element_state::( + global_id, + |interactive_state, window| { + let mut interactive_state = interactive_state + .map(|interactive_state| interactive_state.unwrap_or_default()); + + if let Some(interactive_state) = interactive_state.as_mut() { + if self.tooltip_builder.is_some() { + self.tooltip_id = + set_tooltip_on_window(&interactive_state.active_tooltip, window); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + interactive_state.long_press_tooltip_active.set(false); + interactive_state.active_tooltip.take(); + } + } + + self.text + .prepaint(None, inspector_id, bounds, state, window, cx); + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); + (hitbox, interactive_state) + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut Self::RequestLayoutState, + hitbox: &mut Hitbox, + window: &mut Window, + cx: &mut App, + ) { + let current_view = window.current_view(); + let text_layout = self.text.layout().clone(); + window.with_element_state::( + global_id.unwrap(), + |interactive_state, window| { + let mut interactive_state = interactive_state.unwrap_or_default(); + if let Some(click_listener) = self.click_listener.take() { + let mouse_position = window.mouse_position(); + if let Ok(ix) = text_layout.index_for_position(mouse_position) + && self + .clickable_ranges + .iter() + .any(|range| range.contains(&ix)) + { + window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox) + } + + let text_layout = text_layout.clone(); + let mouse_down = interactive_state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + let hitbox = hitbox.clone(); + let clickable_ranges = mem::take(&mut self.clickable_ranges); + window.on_mouse_event( + move |event: &MouseUpEvent, phase, window: &mut Window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if let Ok(mouse_up_index) = + text_layout.index_for_position(event.position) + { + click_listener( + &clickable_ranges, + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + window, + cx, + ) + } + + mouse_down.take(); + window.refresh(); + } + }, + ); + } else { + let hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { + if phase == DispatchPhase::Bubble + && hitbox.is_hovered(window) + && let Ok(mouse_down_index) = + text_layout.index_for_position(event.position) + { + mouse_down.set(Some(mouse_down_index)); + window.refresh(); + } + }); + } + } + + window.on_mouse_event({ + let mut hover_listener = self.hover_listener.take(); + let hitbox = hitbox.clone(); + let text_layout = text_layout.clone(); + let hovered_index = interactive_state.hovered_index.clone(); + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + let current = hovered_index.get(); + let updated = text_layout.index_for_position(event.position).ok(); + if current != updated { + hovered_index.set(updated); + if let Some(hover_listener) = hover_listener.as_ref() { + hover_listener(updated, event.clone(), window, cx); + } + cx.notify(current_view); + } + } + } + }); + + if let Some(tooltip_builder) = self.tooltip_builder.clone() { + let active_tooltip = interactive_state.active_tooltip.clone(); + let build_tooltip = Rc::new({ + let tooltip_is_hoverable = false; + let text_layout = text_layout.clone(); + move |window: &mut Window, cx: &mut App| { + text_layout + .index_for_position(window.mouse_position()) + .ok() + .and_then(|position| tooltip_builder(position, window, cx)) + .map(|view| (view, tooltip_is_hoverable)) + } + }); + + // Use bounds instead of testing hitbox since this is called during prepaint. + let check_is_hovered_during_prepaint = Rc::new({ + let source_bounds = hitbox.bounds; + let text_layout = text_layout.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + move |window: &Window| { + text_layout + .index_for_position(window.mouse_position()) + .is_ok() + && source_bounds.contains(&window.mouse_position()) + && pending_mouse_down.get().is_none() + } + }); + + let check_is_hovered = Rc::new({ + let hitbox = hitbox.clone(); + let text_layout = text_layout.clone(); + let pending_mouse_down = interactive_state.mouse_down_index.clone(); + move |window: &Window| { + text_layout + .index_for_position(window.mouse_position()) + .is_ok() + && hitbox.is_hovered(window) + && pending_mouse_down.get().is_none() + } + }); + + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + check_is_hovered_during_prepaint, + interactive_state.long_press_tooltip_active.clone(), + None, + window, + ); + } + + self.text + .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx); + + ((), interactive_state) + }, + ); + } +} + +impl IntoElement for InteractiveText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_into_element_for() { + use crate::{ParentElement as _, SharedString, div}; + use std::borrow::Cow; + + let _ = div().child("static str"); + let _ = div().child("String".to_string()); + let _ = div().child(Cow::Borrowed("Cow")); + let _ = div().child(SharedString::from("SharedString")); + } + + #[test] + fn text_macro_id() { + // one call to `text!` = one id + fn make_text_stable_id(happy: bool) -> Text { + text!(if happy { "happy" } else { "sad" }) + } + + // two calls to `text!` = two ids + fn make_text_unstable_id(happy: bool) -> Text { + if happy { text!("happy") } else { text!("sad") } + } + + assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id); + assert_ne!( + make_text_unstable_id(false).id, + make_text_unstable_id(true).id + ); + } +} diff --git a/vendor/gpui-pre/src/elements/uniform_list.rs b/vendor/gpui-pre/src/elements/uniform_list.rs new file mode 100644 index 0000000..0a449a2 --- /dev/null +++ b/vendor/gpui-pre/src/elements/uniform_list.rs @@ -0,0 +1,865 @@ +//! A scrollable list of elements with uniform height, optimized for large lists. +//! Rather than use the full taffy layout system, uniform_list simply measures +//! the first element and then lays out all remaining elements in a line based on that +//! measurement. This is much faster than the full layout system, but only works for +//! elements with uniform height. + +use crate::{ + AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity, + GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, + IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, + StyleRefinement, Styled, Window, point, px, size, +}; +use smallvec::SmallVec; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; + +use super::ListHorizontalSizingBehavior; + +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visible subset of items. +#[track_caller] +pub fn uniform_list( + id: impl Into, + item_count: usize, + f: impl 'static + Fn(Range, &mut Window, &mut App) -> Vec, +) -> UniformList +where + R: IntoElement, +{ + let id = id.into(); + let mut base_style = StyleRefinement::default(); + base_style.overflow.y = Some(Overflow::Scroll); + + let render_range = move |range: Range, window: &mut Window, cx: &mut App| { + f(range, window, cx) + .into_iter() + .map(|component| component.into_any_element()) + .collect() + }; + + UniformList { + item_count, + item_to_measure_index: 0, + render_items: Box::new(render_range), + decorations: Vec::new(), + interactivity: Interactivity { + element_id: Some(id), + base_style: Box::new(base_style), + ..Interactivity::new() + }, + scroll_handle: None, + sizing_behavior: ListSizingBehavior::default(), + horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(), + } +} + +/// A list element for efficiently laying out and displaying a list of uniform-height elements. +pub struct UniformList { + item_count: usize, + item_to_measure_index: usize, + render_items: Box< + dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, + >, + decorations: Vec>, + interactivity: Interactivity, + scroll_handle: Option, + sizing_behavior: ListSizingBehavior, + horizontal_sizing_behavior: ListHorizontalSizingBehavior, +} + +/// Frame state used by the [UniformList]. +pub struct UniformListFrameState { + items: SmallVec<[AnyElement; 32]>, + decorations: SmallVec<[AnyElement; 2]>, +} + +/// A handle for controlling the scroll position of a uniform list. +/// This should be stored in your view and passed to the uniform_list on each frame. +#[derive(Clone, Debug, Default)] +pub struct UniformListScrollHandle(pub Rc>); + +/// Where to place the element scrolled to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScrollStrategy { + /// Place the element at the top of the list's viewport. + Top, + /// Attempt to place the element in the middle of the list's viewport. + /// May not be possible if there's not enough list items above the item scrolled to: + /// in this case, the element will be placed at the closest possible position. + Center, + /// Attempt to place the element at the bottom of the list's viewport. + /// May not be possible if there's not enough list items above the item scrolled to: + /// in this case, the element will be placed at the closest possible position. + Bottom, + /// If the element is not visible attempt to place it at: + /// - The top of the list's viewport if the target element is above currently visible elements. + /// - The bottom of the list's viewport if the target element is above currently visible elements. + Nearest, +} + +#[derive(Clone, Copy, Debug)] +#[allow(missing_docs)] +pub struct DeferredScrollToItem { + /// The item index to scroll to + pub item_index: usize, + /// The scroll strategy to use + pub strategy: ScrollStrategy, + /// The offset in number of items + pub offset: usize, + pub scroll_strict: bool, +} + +#[derive(Clone, Debug, Default)] +#[allow(missing_docs)] +pub struct UniformListScrollState { + pub base_handle: ScrollHandle, + pub deferred_scroll_to_item: Option, + /// Size of the item, captured during last layout. + pub last_item_size: Option, + /// Whether the list was vertically flipped during last layout. + pub y_flipped: bool, +} + +#[derive(Copy, Clone, Debug, Default)] +/// The size of the item and its contents. +pub struct ItemSize { + /// The size of the item. + pub item: Size, + /// The size of the item's contents, which may be larger than the item itself, + /// if the item was bounded by a parent element. + pub contents: Size, +} + +impl UniformListScrollHandle { + /// Create a new scroll handle to bind to a uniform list. + pub fn new() -> Self { + Self(Rc::new(RefCell::new(UniformListScrollState { + base_handle: ScrollHandle::new(), + deferred_scroll_to_item: None, + last_item_size: None, + y_flipped: false, + }))) + } + + /// Scroll the list so that the given item index is visible. + /// + /// This uses non-strict scrolling: if the item is already fully visible, no scrolling occurs. + /// If the item is out of view, it scrolls the minimum amount to bring it into view according + /// to the strategy. + pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + scroll_strict: false, + }); + } + + /// Scroll the list so that the given item index is at scroll strategy position. + /// + /// This uses strict scrolling: the item will always be scrolled to match the strategy position, + /// even if it's already visible. Use this when you need precise positioning. + pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + scroll_strict: true, + }); + } + + /// Scroll the list to the given item index with an offset in number of items. + /// + /// This uses non-strict scrolling: if the item is already visible within the offset region, + /// no scrolling occurs. + /// + /// The offset parameter shrinks the effective viewport by the specified number of items + /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: + /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top + /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport + /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom + pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + scroll_strict: false, + }); + } + + /// Scroll the list so that the given item index is at the exact scroll strategy position with an offset. + /// + /// This uses strict scrolling: the item will always be scrolled to match the strategy position, + /// even if it's already visible. + /// + /// The offset parameter shrinks the effective viewport by the specified number of items + /// from the corresponding edge, then applies the scroll strategy within that reduced viewport: + /// - `ScrollStrategy::Top`: Shrinks from top, positions item at the new top + /// - `ScrollStrategy::Center`: Shrinks from top, centers item in the reduced viewport + /// - `ScrollStrategy::Bottom`: Shrinks from bottom, positions item at the new bottom + pub fn scroll_to_item_strict_with_offset( + &self, + ix: usize, + strategy: ScrollStrategy, + offset: usize, + ) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + scroll_strict: true, + }); + } + + /// Check if the list is flipped vertically. + pub fn y_flipped(&self) -> bool { + self.0.borrow().y_flipped + } + + /// Get the index of the topmost visible child. + #[cfg(any(test, feature = "test-support"))] + pub fn logical_scroll_top_index(&self) -> usize { + let this = self.0.borrow(); + this.deferred_scroll_to_item + .as_ref() + .map(|deferred| deferred.item_index) + .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) + } + + /// Checks if the list can be scrolled vertically. + pub fn is_scrollable(&self) -> bool { + if let Some(size) = self.0.borrow().last_item_size { + size.contents.height > size.item.height + } else { + false + } + } + + /// Whether the list is scrolled to the end, or `None` if the list is + /// not scrollable. + pub fn is_scrolled_to_end(&self) -> Option { + let state = self.0.borrow(); + let max_offset = state.base_handle.max_offset(); + if max_offset.y <= px(0.) { + return None; + } + let offset = state.base_handle.offset(); + Some(-offset.y >= max_offset.y) + } + + /// Scroll to the bottom of the list. + pub fn scroll_to_bottom(&self) { + self.scroll_to_item(usize::MAX, ScrollStrategy::Bottom); + } +} + +impl Styled for UniformList { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style + } +} + +impl Element for UniformList { + type RequestLayoutState = UniformListFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let max_items = self.item_count; + let item_size = self.measure_item(None, window, cx); + let layout_id = self.interactivity.request_layout( + global_id, + inspector_id, + window, + cx, + |style, window, cx| match self.sizing_behavior { + ListSizingBehavior::Infer => { + window.with_text_style(style.text_style().cloned(), |window| { + window.request_measured_layout( + style, + move |known_dimensions, available_space, _window, _cx| { + let desired_height = item_size.height * max_items; + let width = known_dimensions.width.unwrap_or(match available_space + .width + { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(height) => desired_height.min(height), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + desired_height + } + }; + size(width, height) + }, + ) + }) + } + ListSizingBehavior::Auto => window + .with_text_style(style.text_style().cloned(), |window| { + window.request_layout(style, None, cx) + }), + }, + ); + + ( + layout_id, + UniformListFrameState { + items: SmallVec::new(), + decorations: SmallVec::new(), + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + frame_state: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + let style = self + .interactivity + .compute_style(global_id, None, window, cx); + let border = style.border_widths.to_pixels(window.rem_size()); + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.bottom_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + let can_scroll_horizontally = matches!( + self.horizontal_sizing_behavior, + ListHorizontalSizingBehavior::Unconstrained + ); + + let longest_item_size = self.measure_item(None, window, cx); + let content_width = if can_scroll_horizontally { + padded_bounds.size.width.max(longest_item_size.width) + } else { + padded_bounds.size.width + }; + let content_size = Size { + width: content_width, + height: longest_item_size.height * self.item_count, + }; + + let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); + let item_height = longest_item_size.height; + let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| { + let mut handle = handle.0.borrow_mut(); + handle.last_item_size = Some(ItemSize { + item: padded_bounds.size, + contents: content_size, + }); + handle.deferred_scroll_to_item.take() + }); + + self.interactivity.prepaint( + global_id, + inspector_id, + bounds, + content_size, + window, + cx, + |_style, mut scroll_offset, hitbox, window, cx| { + let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { + let scroll_state = scroll_handle.0.borrow(); + scroll_state.y_flipped + } else { + false + }; + + if self.item_count > 0 { + let content_height = item_height * self.item_count; + + let is_scrolled_vertically = !scroll_offset.y.is_zero(); + let max_scroll_offset = padded_bounds.size.height - content_height; + + if is_scrolled_vertically && scroll_offset.y < max_scroll_offset { + shared_scroll_offset.borrow_mut().y = max_scroll_offset; + scroll_offset.y = max_scroll_offset; + } + + let content_width = content_size.width + padding.left + padding.right; + let is_scrolled_horizontally = + can_scroll_horizontally && !scroll_offset.x.is_zero(); + if is_scrolled_horizontally && content_width <= padded_bounds.size.width { + shared_scroll_offset.borrow_mut().x = Pixels::ZERO; + scroll_offset.x = Pixels::ZERO; + } + + if let Some(DeferredScrollToItem { + mut item_index, + mut strategy, + offset, + scroll_strict, + }) = shared_scroll_to_item + { + if y_flipped { + item_index = self.item_count.saturating_sub(item_index + 1); + } + let list_height = padded_bounds.size.height; + let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); + let item_top = item_height * item_index; + let item_bottom = item_top + item_height; + let scroll_top = -updated_scroll_offset.y; + let offset_pixels = item_height * offset; + + // is the selected item above/below currently visible items + let is_above = item_top < scroll_top + offset_pixels; + let is_below = item_bottom > scroll_top + list_height; + + if scroll_strict || is_above || is_below { + if strategy == ScrollStrategy::Nearest { + if is_above { + strategy = ScrollStrategy::Top; + } else if is_below { + strategy = ScrollStrategy::Bottom; + } + } + + let max_scroll_offset = + (content_height - list_height).max(Pixels::ZERO); + match strategy { + ScrollStrategy::Top => { + updated_scroll_offset.y = -(item_top - offset_pixels) + .clamp(Pixels::ZERO, max_scroll_offset); + } + ScrollStrategy::Center => { + let item_center = item_top + item_height / 2.0; + + let viewport_height = list_height - offset_pixels; + let viewport_center = offset_pixels + viewport_height / 2.0; + let target_scroll_top = item_center - viewport_center; + updated_scroll_offset.y = + -target_scroll_top.clamp(Pixels::ZERO, max_scroll_offset); + } + ScrollStrategy::Bottom => { + updated_scroll_offset.y = -(item_bottom - list_height) + .clamp(Pixels::ZERO, max_scroll_offset); + } + ScrollStrategy::Nearest => { + // Nearest, but the item is visible -> no scroll is required + } + } + } + scroll_offset = *updated_scroll_offset + } + + let first_visible_element_ix = + (-(scroll_offset.y + padding.top) / item_height).floor() as usize; + let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) + / item_height) + .ceil() as usize; + + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.item_count); + + let items = if y_flipped { + let flipped_range = self.item_count.saturating_sub(visible_range.end) + ..self.item_count.saturating_sub(visible_range.start); + let mut items = (self.render_items)(flipped_range, window, cx); + items.reverse(); + items + } else { + (self.render_items)(visible_range.clone(), window, cx) + }; + + let content_mask = ContentMask { bounds }; + window.with_content_mask(Some(content_mask), |window| { + for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { + let item_origin = padded_bounds.origin + + scroll_offset + + point(Pixels::ZERO, item_height * ix); + + let available_width = if can_scroll_horizontally { + padded_bounds.size.width + scroll_offset.x.abs() + } else { + padded_bounds.size.width + }; + let available_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::Definite(item_height), + ); + item.layout_as_root(available_space, window, cx); + item.prepaint_at(item_origin, window, cx); + frame_state.items.push(item); + } + + let bounds = + Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size); + for decoration in &self.decorations { + let mut decoration = decoration.as_ref().compute( + visible_range.clone(), + bounds, + scroll_offset, + item_height, + self.item_count, + window, + cx, + ); + let available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(bounds.size.height), + ); + decoration.layout_as_root(available_space, window, cx); + decoration.prepaint_at(bounds.origin, window, cx); + frame_state.decorations.push(decoration); + } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Option, + window: &mut Window, + cx: &mut App, + ) { + self.interactivity.paint( + global_id, + inspector_id, + bounds, + hitbox.as_ref(), + window, + cx, + |_, window, cx| { + for item in &mut request_layout.items { + item.paint(window, cx); + } + for decoration in &mut request_layout.decorations { + decoration.paint(window, cx); + } + }, + ) + } +} + +impl IntoElement for UniformList { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// A decoration for a [`UniformList`]. This can be used for various things, +/// such as rendering indent guides, or other visual effects. +pub trait UniformListDecoration { + /// Compute the decoration element, given the visible range of list items, + /// the bounds of the list, and the height of each item. + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement; +} + +impl UniformListDecoration for Entity { + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + self.update(cx, |inner, cx| { + inner.compute( + visible_range, + bounds, + scroll_offset, + item_height, + item_count, + window, + cx, + ) + }) + } +} + +impl UniformList { + /// Selects a specific list item for measurement. + pub fn with_width_from_item(mut self, item_index: Option) -> Self { + self.item_to_measure_index = item_index.unwrap_or(0); + self + } + + /// Sets the sizing behavior, similar to the `List` element. + pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self { + self.sizing_behavior = behavior; + self + } + + /// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally. + /// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will + /// have the size of the widest item and lay out pushing the `end_slot` to the right end. + pub fn with_horizontal_sizing_behavior( + mut self, + behavior: ListHorizontalSizingBehavior, + ) -> Self { + self.horizontal_sizing_behavior = behavior; + match behavior { + ListHorizontalSizingBehavior::FitList => { + self.interactivity.base_style.overflow.x = None; + } + ListHorizontalSizingBehavior::Unconstrained => { + self.interactivity.base_style.overflow.x = Some(Overflow::Scroll); + } + } + self + } + + /// Adds a decoration element to the list. + pub fn with_decoration(mut self, decoration: impl UniformListDecoration + 'static) -> Self { + self.decorations.push(Box::new(decoration)); + self + } + + fn measure_item( + &self, + list_width: Option, + window: &mut Window, + cx: &mut App, + ) -> Size { + if self.item_count == 0 { + return Size::default(); + } + + let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); + let mut items = (self.render_items)(item_ix..item_ix + 1, window, cx); + let Some(mut item_to_measure) = items.pop() else { + return Size::default(); + }; + let available_space = size( + list_width.map_or(AvailableSpace::MaxContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, + ); + item_to_measure.layout_as_root(available_space, window, cx) + } + + /// Track and render scroll state of this list with reference to the given scroll handle. + pub fn track_scroll(mut self, handle: &UniformListScrollHandle) -> Self { + self.interactivity.tracked_scroll_handle = Some(handle.0.borrow().base_handle.clone()); + self.scroll_handle = Some(handle.clone()); + self + } + + /// Sets whether the list is flipped vertically, such that item 0 appears at the bottom. + pub fn y_flipped(mut self, y_flipped: bool) -> Self { + if let Some(ref scroll_handle) = self.scroll_handle { + let mut scroll_state = scroll_handle.0.borrow_mut(); + let mut base_handle = &scroll_state.base_handle; + let offset = base_handle.offset(); + match scroll_state.last_item_size { + Some(last_size) if scroll_state.y_flipped != y_flipped => { + let new_y_offset = + -(offset.y + last_size.contents.height - last_size.item.height); + base_handle.set_offset(point(offset.x, new_y_offset)); + scroll_state.y_flipped = y_flipped; + } + // Handle case where list is initially flipped. + None if y_flipped => { + base_handle.set_offset(point(offset.x, Pixels::MIN)); + scroll_state.y_flipped = y_flipped; + } + _ => {} + } + } + self + } +} + +impl InteractiveElement for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { + &mut self.interactivity + } +} + +#[cfg(test)] +mod test { + use crate::TestAppContext; + + #[gpui::test] + fn test_scroll_strategy_nearest(cx: &mut TestAppContext) { + use crate::{ + Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*, + px, uniform_list, + }; + use std::ops::Range; + + actions!(example, [SelectNext, SelectPrev]); + + struct TestView { + index: usize, + length: usize, + scroll_handle: UniformListScrollHandle, + focus_handle: FocusHandle, + visible_range: Range, + } + + impl TestView { + pub fn select_next( + &mut self, + _: &SelectNext, + window: &mut Window, + _: &mut Context, + ) { + if self.index + 1 == self.length { + self.index = 0 + } else { + self.index += 1; + } + self.scroll_handle + .scroll_to_item(self.index, ScrollStrategy::Nearest); + window.refresh(); + } + + pub fn select_previous( + &mut self, + _: &SelectPrev, + window: &mut Window, + _: &mut Context, + ) { + if self.index == 0 { + self.index = self.length - 1 + } else { + self.index -= 1; + } + self.scroll_handle + .scroll_to_item(self.index, ScrollStrategy::Nearest); + window.refresh(); + } + } + + impl Render for TestView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .id("list-example") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_previous)) + .size_full() + .child( + uniform_list( + "entries", + self.length, + cx.processor(|this, range: Range, _window, _cx| { + this.visible_range = range.clone(); + range + .map(|ix| div().id(ix).h(px(20.0)).child(format!("Item {ix}"))) + .collect() + }), + ) + .track_scroll(&self.scroll_handle) + .h(px(200.0)), + ) + } + } + + let (view, cx) = cx.add_window_view(|window, cx| { + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle, cx); + TestView { + scroll_handle: UniformListScrollHandle::new(), + index: 0, + focus_handle, + length: 47, + visible_range: 0..0, + } + }); + + // 10 out of 47 items are visible + + // First 9 times selecting next item does not scroll + for ix in 1..10 { + cx.dispatch_action(SelectNext); + view.read_with(cx, |view, _| { + assert_eq!(view.index, ix); + assert_eq!(view.visible_range, 0..10); + }) + } + + // Now each time the list scrolls down by 1 + for ix in 10..47 { + cx.dispatch_action(SelectNext); + view.read_with(cx, |view, _| { + assert_eq!(view.index, ix); + assert_eq!(view.visible_range, ix - 9..ix + 1); + }) + } + + // After the last item we move back to the start + cx.dispatch_action(SelectNext); + view.read_with(cx, |view, _| { + assert_eq!(view.index, 0); + assert_eq!(view.visible_range, 0..10); + }); + + // Return to the last element + cx.dispatch_action(SelectPrev); + view.read_with(cx, |view, _| { + assert_eq!(view.index, 46); + assert_eq!(view.visible_range, 37..47); + }); + + // First 9 times selecting previous does not scroll + for ix in (37..46).rev() { + cx.dispatch_action(SelectPrev); + view.read_with(cx, |view, _| { + assert_eq!(view.index, ix); + assert_eq!(view.visible_range, 37..47); + }) + } + + // Now each time the list scrolls up by 1 + for ix in (0..37).rev() { + cx.dispatch_action(SelectPrev); + view.read_with(cx, |view, _| { + assert_eq!(view.index, ix); + assert_eq!(view.visible_range, ix..ix + 10); + }) + } + } +} diff --git a/vendor/gpui-pre/src/executor.rs b/vendor/gpui-pre/src/executor.rs new file mode 100644 index 0000000..78928d0 --- /dev/null +++ b/vendor/gpui-pre/src/executor.rs @@ -0,0 +1,583 @@ +use crate::{ActivityGuard, App, PlatformDispatcher, PlatformScheduler}; +#[cfg(not(target_family = "wasm"))] +use futures::channel::mpsc; +use futures::prelude::*; +use gpui_util::{TryFutureExt, TryFutureExtBacktrace}; +use scheduler::Instant; +use scheduler::Scheduler; +use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc, time::Duration}; +#[cfg(not(target_family = "wasm"))] +use std::{mem, pin::Pin}; + +pub use scheduler::{ + DedicatedExecutor, FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task, +}; + +/// A pointer to the executor that is currently running, +/// for spawning background tasks. +#[derive(Clone)] +pub struct BackgroundExecutor { + inner: scheduler::BackgroundExecutor, + dispatcher: Arc, +} + +/// A pointer to the executor that is currently running, +/// for spawning tasks on the main thread. +#[derive(Clone)] +pub struct ForegroundExecutor { + inner: scheduler::LocalExecutor, + dispatcher: Arc, + #[cfg(feature = "profiler")] + foreground_runnables: Option, + not_send: PhantomData>, +} + +/// Extension trait for `Task>` that adds `detach_and_log_err` with an `&App` context. +/// +/// This trait is automatically implemented for all `Task>` types. +pub trait TaskExt { + /// Run the task to completion in the background and log any errors that occur. + fn detach_and_log_err(self, cx: &App); + /// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error` + /// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted. + fn detach_and_log_err_with_backtrace(self, cx: &App); +} + +impl TaskExt for Task> +where + T: 'static, + E: 'static + std::fmt::Display + std::fmt::Debug, +{ + #[track_caller] + fn detach_and_log_err(self, cx: &App) { + let location = core::panic::Location::caller(); + cx.foreground_executor() + .spawn(self.log_tracked_err(*location)) + .detach(); + } + + #[track_caller] + fn detach_and_log_err_with_backtrace(self, cx: &App) { + let location = *core::panic::Location::caller(); + cx.foreground_executor() + .spawn(self.log_tracked_err_with_backtrace(location)) + .detach(); + } +} + +impl BackgroundExecutor { + /// Creates a new BackgroundExecutor from the given PlatformDispatcher. + pub fn new(dispatcher: Arc) -> Self { + #[cfg(any(test, feature = "test-support"))] + let scheduler: Arc = if let Some(test_dispatcher) = dispatcher.as_test() { + test_dispatcher.scheduler().clone() + } else { + Arc::new(PlatformScheduler::new(dispatcher.clone())) + }; + + #[cfg(not(any(test, feature = "test-support")))] + let scheduler: Arc = Arc::new(PlatformScheduler::new(dispatcher.clone())); + + Self { + inner: scheduler::BackgroundExecutor::new(scheduler), + dispatcher, + } + } + + /// Returns the underlying scheduler::BackgroundExecutor. + /// + /// This is used by Ex to pass the executor to thread/worktree code. + pub fn scheduler_executor(&self) -> scheduler::BackgroundExecutor { + self.inner.clone() + } + + /// Prevents App Nap-style throttling while the returned guard is held. + /// + /// This does not prevent the system from entering idle sleep. + pub fn prevent_app_nap(&self, reason: &str) -> ActivityGuard { + self.dispatcher.prevent_app_nap(reason) + } + + /// Enqueues the given future to be run to completion on a background thread. + #[track_caller] + pub fn spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static, + { + self.spawn_with_priority(Priority::default(), future.boxed()) + } + + /// Enqueues the given future to be run to completion on a background thread with the given priority. + /// + /// When `Priority::RealtimeAudio` is used, the task runs on a dedicated thread with + /// realtime scheduling priority, suitable for audio processing. + #[track_caller] + pub fn spawn_with_priority( + &self, + priority: Priority, + future: impl Future + Send + 'static, + ) -> Task + where + R: Send + 'static, + { + if priority == Priority::RealtimeAudio { + self.inner.spawn_realtime(future) + } else { + self.inner.spawn_with_priority(priority, future) + } + } + + /// Runs background tasks that may borrow from their environment and waits for all of them to complete. + /// + /// Dropping the returned future cancels its tasks and synchronously waits for their futures to + /// be destroyed before returning. + #[cfg(not(target_family = "wasm"))] + pub async fn scoped<'scope, F>(&self, scheduler: F) + where + F: FnOnce(&mut Scope<'scope>), + { + let mut scope = Scope::new(self.clone(), Priority::default()); + (scheduler)(&mut scope); + let spawned = mem::take(&mut scope.futures) + .into_iter() + .map(|f| self.spawn_with_priority(scope.priority, f)) + .collect::>(); + for task in spawned { + task.await; + } + } + + /// Runs prioritized background tasks that may borrow from their environment and waits for all + /// of them to complete. + /// + /// Dropping the returned future cancels its tasks and synchronously waits for their futures to + /// be destroyed before returning. + #[cfg(not(target_family = "wasm"))] + pub async fn scoped_priority<'scope, F>(&self, priority: Priority, scheduler: F) + where + F: FnOnce(&mut Scope<'scope>), + { + let mut scope = Scope::new(self.clone(), priority); + (scheduler)(&mut scope); + let spawned = mem::take(&mut scope.futures) + .into_iter() + .map(|f| self.spawn_with_priority(scope.priority, f)) + .collect::>(); + for task in spawned { + task.await; + } + } + + /// Get the current time. + /// + /// Calling this instead of `std::time::Instant::now` allows the use + /// of fake timers in tests. + pub fn now(&self) -> Instant { + self.inner.scheduler().clock().now() + } + + /// Returns a task that will complete after the given duration. + /// Depending on other concurrent tasks the elapsed duration may be longer + /// than requested. + #[track_caller] + pub fn timer(&self, duration: Duration) -> Task<()> { + if duration.is_zero() { + return Task::ready(()); + } + self.spawn(self.inner.scheduler().timer(duration)) + } + + /// In tests, run an arbitrary number of tasks (determined by the SEED environment variable) + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_random_delay(&self) -> impl Future + use<> { + self.dispatcher.as_test().unwrap().simulate_random_delay() + } + + /// In tests, move time forward. This does not run any tasks, but does make `timer`s ready. + #[cfg(any(test, feature = "test-support"))] + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.as_test().unwrap().advance_clock(duration) + } + + /// In tests, run one task. + #[cfg(any(test, feature = "test-support"))] + pub fn tick(&self) -> bool { + self.dispatcher.as_test().unwrap().scheduler().tick() + } + + /// In tests, run tasks until the scheduler would park. + /// + /// Under the scheduler-backed test dispatcher, `tick()` will not advance the clock, so a pending + /// timer can keep `has_pending_tasks()` true even after all currently-runnable tasks have been + /// drained. To preserve the historical semantics that tests relied on (drain all work that can + /// make progress), we advance the clock to the next timer when no runnable tasks remain. + #[cfg(any(test, feature = "test-support"))] + pub fn run_until_parked(&self) { + let scheduler = self.dispatcher.as_test().unwrap().scheduler(); + scheduler.run(); + } + + /// In tests, prevents `run_until_parked` from panicking if there are outstanding tasks. + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .allow_parking(); + + if std::env::var("GPUI_RUN_UNTIL_PARKED_LOG").ok().as_deref() == Some("1") { + log::warn!("[gpui::executor] allow_parking: enabled"); + } + } + + /// Sets the range of ticks to run before timing out in block_on. + #[cfg(any(test, feature = "test-support"))] + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .set_timeout_ticks(range); + } + + /// Undoes the effect of [`Self::allow_parking`]. + #[cfg(any(test, feature = "test-support"))] + pub fn forbid_parking(&self) { + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .forbid_parking(); + } + + /// In tests, returns the rng used by the dispatcher. + #[cfg(any(test, feature = "test-support"))] + pub fn rng(&self) -> scheduler::SharedRng { + self.dispatcher.as_test().unwrap().scheduler().rng() + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + #[cfg(any(test, feature = "test-support"))] + if let Some(test) = self.dispatcher.as_test() { + return test.num_cpus_override().unwrap_or(4); + } + num_cpus::get() + } + + /// Override the number of CPUs reported by this executor in tests. + /// Panics if not called on a test executor. + #[cfg(any(test, feature = "test-support"))] + pub fn set_num_cpus(&self, count: usize) { + self.dispatcher + .as_test() + .expect("set_num_cpus can only be called on a test executor") + .set_num_cpus(count); + } + + /// Whether we're on the main thread. + pub fn is_main_thread(&self) -> bool { + self.dispatcher.is_main_thread() + } + + /// Whether this executor runs on the deterministic test scheduler. + /// + /// Available in every build. Use it, rather than `cfg(feature = + /// "test-support")`, for behavior that must differ under tests, so that + /// compiling `test-support` into an application doesn't change it. + pub fn is_test(&self) -> bool { + self.inner.is_test() + } + + #[doc(hidden)] + pub fn dispatcher(&self) -> &Arc { + &self.dispatcher + } +} + +impl ForegroundExecutor { + /// Creates a new ForegroundExecutor from the given PlatformDispatcher. + pub fn new(dispatcher: Arc) -> Self { + #[cfg(any(test, feature = "test-support"))] + let (scheduler, session_id): (Arc, _) = + if let Some(test_dispatcher) = dispatcher.as_test() { + ( + test_dispatcher.scheduler().clone(), + test_dispatcher.session_id(), + ) + } else { + let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); + let inner = platform_scheduler.foreground_executor(); + return Self { + inner, + dispatcher, + #[cfg(feature = "profiler")] + foreground_runnables: Some(platform_scheduler.foreground_runnable_counter()), + not_send: PhantomData, + }; + }; + + #[cfg(not(any(test, feature = "test-support")))] + let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); + #[cfg(not(any(test, feature = "test-support")))] + let inner = platform_scheduler.foreground_executor(); + #[cfg(all(not(any(test, feature = "test-support")), feature = "profiler"))] + let foreground_runnables = Some(platform_scheduler.foreground_runnable_counter()); + + #[cfg(any(test, feature = "test-support"))] + let inner = { + let scheduler_for_dispatch = Arc::downgrade(&scheduler); + scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| { + if let Some(scheduler) = scheduler_for_dispatch.upgrade() { + scheduler.schedule_local(session_id, runnable); + } + }) + }; + + #[cfg(all(any(test, feature = "test-support"), feature = "profiler"))] + // The deterministic test scheduler does not invoke GPUI's task profiler + // hooks, so an increment here would have no matching decrement. + let foreground_runnables = None; + + Self { + inner, + dispatcher, + #[cfg(feature = "profiler")] + foreground_runnables, + not_send: PhantomData, + } + } + + /// Enqueues the given Task to run on the main thread. + #[track_caller] + pub fn spawn(&self, future: impl Future + 'static) -> Task + where + R: 'static, + { + self.inner.spawn(future.boxed_local()) + } + + /// Enqueues the given Task to run on the main thread with the given priority. + #[track_caller] + pub fn spawn_with_priority( + &self, + _priority: Priority, + future: impl Future + 'static, + ) -> Task + where + R: 'static, + { + // Priority is ignored for foreground tasks - they run in order on the main thread + self.inner.spawn(future) + } + + /// On platforms with dedicated support, enqueues the given future to run + /// on the main thread during platform idle time. Without a `timeout`, + /// polls may be deferred indefinitely while the platform stays busy; + /// with one, a poll still waiting after that long runs as ordinary main-thread work. + /// Each poll occupies part of one idle slice, so long synchronous stretches + /// should bound themselves against [`Self::idle_time_remaining`] and yield. + /// + /// On platforms without dedicated support, schedules the given future to run + /// with a low priority, ignoring `timeout`. + #[track_caller] + pub fn spawn_when_idle( + &self, + timeout: Option, + future: impl Future + 'static, + ) -> Task + where + R: 'static, + { + let dispatcher = self.dispatcher.clone(); + #[cfg(feature = "profiler")] + let foreground_runnables = self.foreground_runnables.clone(); + self.inner + .spawn_with_dispatch(future.boxed_local(), move |runnable| { + #[cfg(feature = "profiler")] + if let Some(foreground_runnables) = &foreground_runnables { + foreground_runnables.queued(); + } + dispatcher.dispatch_on_main_thread_when_idle(runnable, timeout); + }) + } + + /// The time remaining in the current idle slice, when called from a task + /// spawned via [`Self::spawn_when_idle`] on a platform that meters idle + /// time. `None` when idle time is unmetered (or the caller is not inside + /// an idle slice); work that must bound itself should then fall back to a + /// budget of its own. + pub fn idle_time_remaining(&self) -> Option { + self.dispatcher.idle_time_remaining() + } + + /// Used by the test harness to run an async test in a synchronous fashion. + #[cfg(all(not(target_family = "wasm"), any(test, feature = "test-support")))] + #[track_caller] + pub fn block_test(&self, future: impl Future) -> R { + use std::cell::Cell; + + let scheduler = self.inner.scheduler(); + + let output = Cell::new(None); + let future = async { + output.set(Some(future.await)); + }; + let mut future = std::pin::pin!(future); + + // In async GPUI tests, we must allow foreground tasks scheduled by the test itself + // (which are associated with the test session) to make progress while we block. + // Otherwise, awaiting futures that depend on same-session foreground work can deadlock. + scheduler.block(None, future.as_mut(), None); + + output.take().expect("block_test future did not complete") + } + + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. + #[cfg(not(target_family = "wasm"))] + pub fn block_on(&self, future: impl Future) -> R { + self.inner.block_on(future) + } + + /// Block the current thread until the given future resolves or the timeout elapses. + #[cfg(not(target_family = "wasm"))] + pub fn block_with_timeout>( + &self, + duration: Duration, + future: Fut, + ) -> Result + use> { + self.inner.block_with_timeout(duration, future) + } + + #[doc(hidden)] + pub fn dispatcher(&self) -> &Arc { + &self.dispatcher + } + + #[doc(hidden)] + pub fn scheduler_executor(&self) -> SchedulerLocalExecutor { + self.inner.clone() + } +} + +/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. +#[cfg(not(target_family = "wasm"))] +pub struct Scope<'a> { + executor: BackgroundExecutor, + priority: Priority, + futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, + lifetime: PhantomData<&'a ()>, +} + +#[cfg(not(target_family = "wasm"))] +impl<'a> Scope<'a> { + fn new(executor: BackgroundExecutor, priority: Priority) -> Self { + let (tx, rx) = mpsc::channel(1); + Self { + executor, + priority, + tx: Some(tx), + rx, + futures: Default::default(), + lifetime: PhantomData, + } + } + + /// How many CPUs are available to the dispatcher. + pub fn num_cpus(&self) -> usize { + self.executor.num_cpus() + } + + /// Spawn a future into this scope. + #[track_caller] + pub fn spawn(&mut self, f: F) + where + F: Future + Send + 'a, + { + let tx = self.tx.clone().unwrap(); + + // SAFETY: The 'a lifetime is guaranteed to outlive any of these futures because + // dropping this `Scope` blocks until all of the futures have resolved. + let f = unsafe { + mem::transmute::< + Pin + Send + 'a>>, + Pin + Send + 'static>>, + >(Box::pin(async move { + f.await; + drop(tx); + })) + }; + self.futures.push(f); + } +} + +#[cfg(not(target_family = "wasm"))] +impl Drop for Scope<'_> { + fn drop(&mut self) { + self.tx.take().unwrap(); + + // Wait until the channel is closed, which means that all of the spawned + // futures have resolved. + let future = async { + self.rx.next().await; + }; + let mut future = std::pin::pin!(future); + self.executor + .inner + .scheduler() + .block(None, future.as_mut(), None); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{App, TestDispatcher, TestPlatform}; + use std::cell::RefCell; + + /// Helper to create test infrastructure. + /// Returns (dispatcher, background_executor, app). + fn create_test_app() -> (TestDispatcher, BackgroundExecutor, Rc) { + let dispatcher = TestDispatcher::new(0); + let arc_dispatcher = Arc::new(dispatcher.clone()); + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + + let platform = TestPlatform::new(background_executor.clone(), foreground_executor); + let asset_source = Arc::new(()); + let http_client = http_client::FakeHttpClient::with_404_response(); + + let app = App::new_app(platform, asset_source, http_client); + (dispatcher, background_executor, app) + } + + #[test] + fn sanity_test_tasks_run() { + let (dispatcher, _background_executor, app) = create_test_app(); + let foreground_executor = app.borrow().foreground_executor.clone(); + + let task_ran = Rc::new(RefCell::new(false)); + + foreground_executor + .spawn({ + let task_ran = Rc::clone(&task_ran); + async move { + *task_ran.borrow_mut() = true; + } + }) + .detach(); + + // Run dispatcher while app is still alive + dispatcher.run_until_parked(); + + // Task should have run + assert!( + *task_ran.borrow(), + "Task should run normally when app is alive" + ); + } +} diff --git a/vendor/gpui-pre/src/geometry.rs b/vendor/gpui-pre/src/geometry.rs new file mode 100644 index 0000000..1c87031 --- /dev/null +++ b/vendor/gpui-pre/src/geometry.rs @@ -0,0 +1,4020 @@ +//! The GPUI geometry module is a collection of types and traits that +//! can be used to describe common units, concepts, and the relationships +//! between them. + +use anyhow::{Context as _, anyhow}; +use core::fmt::Debug; +use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign}; +use refineable::Refineable; +use schemars::{JsonSchema, json_schema}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; +use std::borrow::Cow; +use std::ops::{AddAssign, Range}; +use std::{ + cmp::{self, PartialOrd}, + fmt::{self, Display}, + hash::Hash, + ops::{Add, Div, Mul, MulAssign, Neg, Sub}, +}; +use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; + +use crate::{App, DisplayId}; + +/// Axis in a 2D cartesian space. +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] +pub enum Axis { + /// The y axis, or up and down + Vertical, + /// The x axis, or left and right + Horizontal, +} + +impl Axis { + /// Swap this axis to the opposite axis. + pub fn invert(self) -> Self { + match self { + Axis::Vertical => Axis::Horizontal, + Axis::Horizontal => Axis::Vertical, + } + } +} + +/// A trait for accessing the given unit along a certain axis. +pub trait Along { + /// The unit associated with this type + type Unit; + + /// Returns the unit along the given axis. + fn along(&self, axis: Axis) -> Self::Unit; + + /// Applies the given function to the unit along the given axis and returns a new value. + fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self; +} + +/// Describes a location in a 2D cartesian space. +/// +/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. +/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. +/// +/// # Examples +/// +/// ``` +/// # use gpui::Point; +/// let point = Point { x: 10, y: 20 }; +/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } +/// ``` +#[derive( + Refineable, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Copy, + Debug, + PartialEq, + Eq, + Serialize, + Deserialize, + JsonSchema, + Hash, + Neg, +)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Point { + /// The x coordinate of the point. + pub x: T, + /// The y coordinate of the point. + pub y: T, +} + +/// Constructs a new `Point` with the given x and y coordinates. +/// +/// # Arguments +/// +/// * `x` - The x coordinate of the point. +/// * `y` - The y coordinate of the point. +/// +/// # Returns +/// +/// Returns a `Point` with the specified coordinates. +/// +/// # Examples +/// +/// ``` +/// use gpui::point; +/// let p = point(10, 20); +/// assert_eq!(p.x, 10); +/// assert_eq!(p.y, 20); +/// ``` +pub const fn point(x: T, y: T) -> Point { + Point { x, y } +} + +impl Point { + /// Creates a new `Point` with the specified `x` and `y` coordinates. + /// + /// # Arguments + /// + /// * `x` - The horizontal coordinate of the point. + /// * `y` - The vertical coordinate of the point. + /// + /// # Examples + /// + /// ``` + /// use gpui::Point; + /// let p = Point::new(10, 20); + /// assert_eq!(p.x, 10); + /// assert_eq!(p.y, 20); + /// ``` + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + /// Transforms the point to a `Point` by applying the given function to both coordinates. + /// + /// This method allows for converting a `Point` to a `Point` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `x` + /// and `y` coordinates, resulting in a new point of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p = Point { x: 3, y: 4 }; + /// let p_float = p.map(|coord| coord as f32); + /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); + /// ``` + #[must_use] + pub fn map(&self, f: impl Fn(T) -> U) -> Point { + Point { + x: f(self.x.clone()), + y: f(self.y.clone()), + } + } +} + +impl Along for Point { + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.x.clone(), + Axis::Vertical => self.y.clone(), + } + } + + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point { + match axis { + Axis::Horizontal => Point { + x: f(self.x.clone()), + y: self.y.clone(), + }, + Axis::Vertical => Point { + x: self.x.clone(), + y: f(self.y.clone()), + }, + } + } +} + +impl Point { + /// Scales the point by a given factor, which is typically derived from the resolution + /// of a target display to ensure proper sizing of UI elements. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to both the x and y coordinates. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Point, Pixels, ScaledPixels}; + /// let p = Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }; + /// let scaled_p = p.scale(1.5); + /// assert_eq!(scaled_p, Point { x: ScaledPixels::from(15.0), y: ScaledPixels::from(30.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Point { + Point { + x: self.x.scale(factor), + y: self.y.scale(factor), + } + } + + /// Calculates the Euclidean distance from the origin (0, 0) to this point. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Pixels, Point}; + /// let p = Point { x: Pixels::from(3.0), y: Pixels::from(4.0) }; + /// assert_eq!(p.magnitude(), 5.0); + /// ``` + pub fn magnitude(&self) -> f64 { + ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() + } +} + +impl Point +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + /// Get the position of this point, relative to the given origin + pub fn relative_to(&self, origin: &Point) -> Point { + point( + self.x.clone() - origin.x.clone(), + self.y.clone() - origin.y.clone(), + ) + } +} + +impl Mul for Point +where + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug, +{ + type Output = Point; + + fn mul(self, rhs: Rhs) -> Self::Output { + Point { + x: self.x * rhs.clone(), + y: self.y * rhs, + } + } +} + +impl MulAssign for Point +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.x = self.x.clone() * rhs.clone(); + self.y = self.y.clone() * rhs; + } +} + +impl Div for Point +where + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self::Output { + Self { + x: self.x / rhs.clone(), + y: self.y / rhs, + } + } +} + +impl Point +where + T: PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Returns a new point with the maximum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let max_point = p1.max(&p2); + /// assert_eq!(max_point, Point { x: 5, y: 7 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Point { + x: if self.x > other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y > other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Returns a new point with the minimum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let min_point = p1.min(&p2); + /// assert_eq!(min_point, Point { x: 3, y: 2 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Point { + x: if self.x <= other.x { + self.x.clone() + } else { + other.x.clone() + }, + y: if self.y <= other.y { + self.y.clone() + } else { + other.y.clone() + }, + } + } + + /// Clamps the point to a specified range. + /// + /// Given a minimum point and a maximum point, this method constrains the current point + /// such that its coordinates do not exceed the range defined by the minimum and maximum points. + /// If the current point's coordinates are less than the minimum, they are set to the minimum. + /// If they are greater than the maximum, they are set to the maximum. + /// + /// # Arguments + /// + /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. + /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Point; + /// let p = Point { x: 10, y: 20 }; + /// let min = Point { x: 0, y: 5 }; + /// let max = Point { x: 15, y: 25 }; + /// let clamped_p = p.clamp(&min, &max); + /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); + /// + /// let p_out_of_bounds = Point { x: -5, y: 30 }; + /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); + /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); + /// ``` + pub fn clamp(&self, min: &Self, max: &Self) -> Self { + self.max(min).min(max) + } +} + +impl Clone for Point { + fn clone(&self) -> Self { + Self { + x: self.x.clone(), + y: self.y.clone(), + } + } +} + +impl Display for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +/// A structure representing a two-dimensional size with width and height in a given unit. +/// +/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. +/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. +#[derive( + Add, Clone, Copy, Default, Deserialize, Div, Hash, Neg, PartialEq, Refineable, Serialize, Sub, +)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Size { + /// The width component of the size. + pub width: T, + /// The height component of the size. + pub height: T, +} + +impl Size { + /// Create a new Size, a synonym for [`size`] + pub fn new(width: T, height: T) -> Self { + size(width, height) + } +} + +/// Constructs a new `Size` with the provided width and height. +/// +/// # Arguments +/// +/// * `width` - The width component of the `Size`. +/// * `height` - The height component of the `Size`. +/// +/// # Examples +/// +/// ``` +/// use gpui::size; +/// let my_size = size(10, 20); +/// assert_eq!(my_size.width, 10); +/// assert_eq!(my_size.height, 20); +/// ``` +pub const fn size(width: T, height: T) -> Size +where + T: Clone + Debug + Default + PartialEq, +{ + Size { width, height } +} + +impl Size +where + T: Clone + Debug + Default + PartialEq, +{ + /// Applies a function to the width and height of the size, producing a new `Size`. + /// + /// This method allows for converting a `Size` to a `Size` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `width` + /// and `height`, resulting in a new size of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let my_size = Size { width: 10, height: 20 }; + /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); + /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Size + where + U: Clone + Debug + Default + PartialEq, + { + Size { + width: f(self.width.clone()), + height: f(self.height.clone()), + } + } +} + +impl Size +where + T: Clone + Debug + Default + PartialEq + Half, +{ + /// Compute the center point of the size.g + pub fn center(&self) -> Point { + Point { + x: self.width.half(), + y: self.height.half(), + } + } +} + +impl Size { + /// Scales the size by a given factor. + /// + /// This method multiplies both the width and height by the provided scaling factor, + /// resulting in a new `Size` that is proportionally larger or smaller + /// depending on the factor. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the width and height. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Size, Pixels, ScaledPixels}; + /// let size = Size { width: Pixels::from(100.0), height: Pixels::from(50.0) }; + /// let scaled_size = size.scale(2.0); + /// assert_eq!(scaled_size, Size { width: ScaledPixels::from(200.0), height: ScaledPixels::from(100.0) }); + /// ``` + pub fn scale(&self, factor: f32) -> Size { + Size { + width: self.width.scale(factor), + height: self.height.scale(factor), + } + } +} + +impl Along for Size +where + T: Clone + Debug + Default + PartialEq, +{ + type Unit = T; + + fn along(&self, axis: Axis) -> T { + match axis { + Axis::Horizontal => self.width.clone(), + Axis::Vertical => self.height.clone(), + } + } + + /// Returns the value of this size along the given axis. + fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self { + match axis { + Axis::Horizontal => Size { + width: f(self.width.clone()), + height: self.height.clone(), + }, + Axis::Vertical => Size { + width: self.width.clone(), + height: f(self.height.clone()), + }, + } + } +} + +impl Size +where + T: PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Returns a new `Size` with the maximum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let max_size = size1.max(&size2); + /// assert_eq!(max_size, Size { width: 50, height: 40 }); + /// ``` + pub fn max(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + self.width.clone() + } else { + other.width.clone() + }, + height: if self.height >= other.height { + self.height.clone() + } else { + other.height.clone() + }, + } + } + + /// Returns a new `Size` with the minimum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let min_size = size1.min(&size2); + /// assert_eq!(min_size, Size { width: 30, height: 20 }); + /// ``` + pub fn min(&self, other: &Self) -> Self { + Size { + width: if self.width >= other.width { + other.width.clone() + } else { + self.width.clone() + }, + height: if self.height >= other.height { + other.height.clone() + } else { + self.height.clone() + }, + } + } +} + +impl Mul for Size +where + T: Mul + Clone + Debug + Default + PartialEq, + Rhs: Clone + Debug + Default + PartialEq, +{ + type Output = Size; + + fn mul(self, rhs: Rhs) -> Self::Output { + Size { + width: self.width * rhs.clone(), + height: self.height * rhs, + } + } +} + +impl MulAssign for Size +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.width = self.width.clone() * rhs.clone(); + self.height = self.height.clone() * rhs; + } +} + +impl Eq for Size where T: Eq + Clone + Debug + Default + PartialEq {} + +impl Debug for Size +where + T: Clone + Debug + Default + PartialEq, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Size {{ {:?} × {:?} }}", self.width, self.height) + } +} + +impl Display for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} × {}", self.width, self.height) + } +} + +impl From> for Size { + fn from(point: Point) -> Self { + Self { + width: point.x, + height: point.y, + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: size.width.into(), + height: size.height.into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to fill the available space. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, + /// which represents 100% of the available space in both dimensions. + /// + /// # Returns + /// + /// A `Size` that will fill the available space when used in a layout. + pub fn full() -> Self { + Self { + width: relative(1.).into(), + height: relative(1.).into(), + } + } +} + +impl Size { + /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, + /// indicating that their size should be computed based on the layout context, such as the content size or + /// available space. + /// + /// # Returns + /// + /// A `Size` with width and height set to `Length::Auto`. + pub fn auto() -> Self { + Self { + width: Length::Auto, + height: Length::Auto, + } + } +} + +/// Represents a rectangular area in a 2D space with an origin point and a size. +/// +/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. +/// The origin is represented as a `Point` which defines the top left corner of the rectangle, +/// and the size is represented as a `Size` which defines the width and height of the rectangle. +/// +/// # Examples +/// +/// ``` +/// # use gpui::{Bounds, Point, Size}; +/// let origin = Point { x: 0, y: 0 }; +/// let size = Size { width: 10, height: 20 }; +/// let bounds = Bounds::new(origin, size); +/// +/// assert_eq!(bounds.origin, origin); +/// assert_eq!(bounds.size, size); +/// ``` +#[derive(Refineable, Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] +#[refineable(Debug)] +#[repr(C)] +pub struct Bounds { + /// The origin point of this area. + pub origin: Point, + /// The size of the rectangle. + pub size: Size, +} + +/// Create a bounds with the given origin and size +pub fn bounds( + origin: Point, + size: Size, +) -> Bounds { + Bounds { origin, size } +} + +impl Bounds { + /// Generate a centered bounds for the given display or primary display if none is provided + pub fn centered(display_id: Option, size: Size, cx: &App) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + display + .map(|display| { + let visible_bounds = display.visible_bounds(); + Bounds::centered_at(visible_bounds.center(), size.min(&visible_bounds.size)) + }) + .unwrap_or_else(|| Bounds { + origin: point(px(0.), px(0.)), + size, + }) + } + + /// Generate maximized bounds for the given display or primary display if none is provided + pub fn maximized(display_id: Option, cx: &App) -> Self { + let display = display_id + .and_then(|id| cx.find_display(id)) + .or_else(|| cx.primary_display()); + + display + .map(|display| display.bounds()) + .unwrap_or_else(|| Bounds { + origin: point(px(0.), px(0.)), + size: size(px(1024.), px(768.)), + }) + } +} + +impl Bounds +where + T: Clone + Debug + Default + PartialEq, +{ + /// Creates a new `Bounds` with the specified origin and size. + /// + /// # Arguments + /// + /// * `origin` - A `Point` representing the origin of the bounds. + /// * `size` - A `Size` representing the size of the bounds. + /// + /// # Returns + /// + /// Returns a `Bounds` that has the given origin and size. + pub fn new(origin: Point, size: Size) -> Self { + Bounds { origin, size } + } +} + +impl Bounds +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + /// Constructs a `Bounds` from two corner points: the top left and bottom right corners. + /// + /// This function calculates the origin and size of the `Bounds` based on the provided corner points. + /// The origin is set to the top left corner, and the size is determined by the difference between + /// the x and y coordinates of the bottom right and top left points. + /// + /// # Arguments + /// + /// * `top_left` - A `Point` representing the top left corner of the rectangle. + /// * `bottom_right` - A `Point` representing the bottom right corner of the rectangle. + /// + /// # Returns + /// + /// Returns a `Bounds` that encompasses the area defined by the two corner points. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point}; + /// let top_left = Point { x: 0, y: 0 }; + /// let bottom_right = Point { x: 10, y: 10 }; + /// let bounds = Bounds::from_corners(top_left, bottom_right); + /// + /// assert_eq!(bounds.origin, top_left); + /// assert_eq!(bounds.size.width, 10); + /// assert_eq!(bounds.size.height, 10); + /// ``` + pub fn from_corners(top_left: Point, bottom_right: Point) -> Self { + let origin = Point { + x: top_left.x.clone(), + y: top_left.y.clone(), + }; + let size = Size { + width: bottom_right.x - top_left.x, + height: bottom_right.y - top_left.y, + }; + Bounds { origin, size } + } +} + +impl Bounds +where + T: Sub + Half + Clone + Debug + Default + PartialEq, +{ + /// Constructs a `Bounds` from a corner point and size. The specified corner will be placed at + /// the specified origin. + pub fn from_anchor_and_size(corner: Anchor, origin: Point, size: Size) -> Bounds { + let origin = match corner { + Anchor::TopLeft => origin, + Anchor::TopRight => Point { + x: origin.x - size.width.clone(), + y: origin.y, + }, + Anchor::BottomLeft => Point { + x: origin.x, + y: origin.y - size.height.clone(), + }, + Anchor::BottomRight => Point { + x: origin.x - size.width.clone(), + y: origin.y - size.height.clone(), + }, + Anchor::TopCenter => Point { + x: origin.x - size.width.half(), + y: origin.y, + }, + Anchor::BottomCenter => Point { + x: origin.x - size.width.half(), + y: origin.y - size.height.clone(), + }, + Anchor::LeftCenter => Point { + x: origin.x, + y: origin.y - size.height.half(), + }, + Anchor::RightCenter => Point { + x: origin.x - size.width.clone(), + y: origin.y - size.height.half(), + }, + }; + + Bounds { origin, size } + } +} + +impl Bounds +where + T: Sub + Half + Clone + Debug + Default + PartialEq, +{ + /// Creates a new bounds centered at the given point. + pub fn centered_at(center: Point, size: Size) -> Self { + let origin = Point { + x: center.x - size.width.half(), + y: center.y - size.height.half(), + }; + Self::new(origin, size) + } +} + +impl Bounds +where + T: Add + Half + Clone + Debug + Default + PartialEq, +{ + /// Returns the top center point of the bounds. + pub fn top_center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.half(), + y: self.origin.y.clone(), + } + } + + /// Returns the bottom center point of the bounds. + pub fn bottom_center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.half(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } + + /// Returns the left center point of the bounds. + pub fn left_center(&self) -> Point { + Point { + x: self.origin.x.clone(), + y: self.origin.y.clone() + self.size.height.half(), + } + } + + /// Returns the right center point of the bounds. + pub fn right_center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone() + self.size.height.half(), + } + } +} + +impl Bounds +where + T: PartialOrd + Add + Clone + Debug + Default + PartialEq, +{ + /// Checks if this `Bounds` intersects with another `Bounds`. + /// + /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. + /// This method checks if there is any overlapping area between the two bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to check for intersection with. + /// + /// # Returns + /// + /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds3 = Bounds { + /// origin: Point { x: 20, y: 20 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds + /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds + /// ``` + pub fn intersects(&self, other: &Bounds) -> bool { + let my_lower_right = self.bottom_right(); + let their_lower_right = other.bottom_right(); + + self.origin.x < their_lower_right.x + && my_lower_right.x > other.origin.x + && self.origin.y < their_lower_right.y + && my_lower_right.y > other.origin.y + } +} + +impl Bounds +where + T: Add + Half + Clone + Debug + Default + PartialEq, +{ + /// Returns the center point of the bounds. + /// + /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height + /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the + /// coordinate system. + /// + /// # Returns + /// + /// A `Point` representing the center of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let center = bounds.center(); + /// assert_eq!(center, Point { x: 5, y: 10 }); + /// ``` + pub fn center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone().half(), + y: self.origin.y.clone() + self.size.height.clone().half(), + } + } +} + +impl Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + /// Calculates the half perimeter of a rectangle defined by the bounds. + /// + /// The half perimeter is calculated as the sum of the width and the height of the rectangle. + /// This method is generic over the type `T` which must implement the `Sub` trait to allow + /// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait + /// to sum the width and height for the half perimeter. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let half_perimeter = bounds.half_perimeter(); + /// assert_eq!(half_perimeter, 30); + /// ``` + pub fn half_perimeter(&self) -> T { + self.size.width.clone() + self.size.height.clone() + } +} + +impl Bounds +where + T: Add + Sub + Clone + Debug + Default + PartialEq, +{ + /// Dilates the bounds by a specified amount in all directions. + /// + /// This method expands the bounds by the given `amount`, increasing the size + /// and adjusting the origin so that the bounds grow outwards equally in all directions. + /// The resulting bounds will have its width and height increased by twice the `amount` + /// (since it grows in both directions), and the origin will be moved by `-amount` + /// in both the x and y directions. + /// + /// # Arguments + /// + /// * `amount` - The amount by which to dilate the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let mut bounds = Bounds { + /// origin: Point { x: 10, y: 10 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let expanded_bounds = bounds.dilate(5); + /// assert_eq!(expanded_bounds, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + #[must_use] + pub fn dilate(&self, amount: T) -> Bounds { + let double_amount = amount.clone() + amount.clone(); + Bounds { + origin: self.origin.clone() - point(amount.clone(), amount), + size: self.size.clone() + size(double_amount.clone(), double_amount), + } + } + + /// Extends the bounds different amounts in each direction. + #[must_use] + pub fn extend(&self, amount: Edges) -> Bounds { + Bounds { + origin: self.origin.clone() - point(amount.left.clone(), amount.top.clone()), + size: self.size.clone() + + size( + amount.left.clone() + amount.right.clone(), + amount.top.clone() + amount.bottom, + ), + } + } +} + +impl Bounds +where + T: Add + + Sub + + Neg + + Clone + + Debug + + Default + + PartialEq, +{ + /// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated. + /// + /// Note that this may panic if T does not support negative values. + pub fn inset(&self, amount: T) -> Self { + self.dilate(-amount) + } +} + +impl + Sub + Clone + Debug + Default + PartialEq> + Bounds +{ + /// Calculates the intersection of two `Bounds` objects. + /// + /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, + /// the resulting `Bounds` will have a size with width and height of zero. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to intersect with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the intersection area. If there is no intersection, + /// the returned `Bounds` will have a size with width and height of zero. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let intersection = bounds1.intersect(&bounds2); + /// + /// assert_eq!(intersection, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 5, height: 5 }, + /// }); + /// ``` + pub fn intersect(&self, other: &Self) -> Self { + let upper_left = self.origin.max(&other.origin); + let bottom_right = self + .bottom_right() + .min(&other.bottom_right()) + .max(&upper_left); + Self::from_corners(upper_left, bottom_right) + } + + /// Computes the union of two `Bounds`. + /// + /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. + /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, + /// and a size that encompasses the furthest extents of both `Bounds`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to create a union with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the union of the two `Bounds`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 15, height: 15 }, + /// }; + /// let union_bounds = bounds1.union(&bounds2); + /// + /// assert_eq!(union_bounds, Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` + pub fn union(&self, other: &Self) -> Self { + let top_left = self.origin.min(&other.origin); + let bottom_right = self.bottom_right().max(&other.bottom_right()); + Bounds::from_corners(top_left, bottom_right) + } +} + +impl Bounds +where + T: Add + Sub + Clone + Debug + Default + PartialEq, +{ + /// Computes the space available within outer bounds. + pub fn space_within(&self, outer: &Self) -> Edges { + Edges { + top: self.top() - outer.top(), + right: outer.right() - self.right(), + bottom: outer.bottom() - self.bottom(), + left: self.left() - outer.left(), + } + } +} + +impl Mul for Bounds +where + T: Mul + Clone + Debug + Default + PartialEq, + Point: Mul>, + Rhs: Clone + Debug + Default + PartialEq, +{ + type Output = Bounds; + + fn mul(self, rhs: Rhs) -> Self::Output { + Bounds { + origin: self.origin * rhs.clone(), + size: self.size * rhs, + } + } +} + +impl MulAssign for Bounds +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.origin *= rhs.clone(); + self.size *= rhs; + } +} + +impl Div for Bounds +where + Size: Div>, + T: Div + Clone + Debug + Default + PartialEq, + S: Clone, +{ + type Output = Self; + + fn div(self, rhs: S) -> Self { + Self { + origin: self.origin / rhs.clone(), + size: self.size / rhs, + } + } +} + +impl Add> for Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn add(self, rhs: Point) -> Self { + Self { + origin: self.origin + rhs, + size: self.size, + } + } +} + +impl Sub> for Bounds +where + T: Sub + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn sub(self, rhs: Point) -> Self { + Self { + origin: self.origin - rhs, + size: self.size, + } + } +} + +impl From> for Point { + fn from(size: Size) -> Self { + Self { + x: size.width, + y: size.height, + } + } +} + +impl Bounds +where + T: Add + Clone + Debug + Default + PartialEq, +{ + /// Returns the top edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the top edge of the bounds. + pub fn top(&self) -> T { + self.origin.y.clone() + } + + /// Returns the bottom edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. + pub fn bottom(&self) -> T { + self.origin.y.clone() + self.size.height.clone() + } + + /// Returns the left edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the left edge of the bounds. + pub fn left(&self) -> T { + self.origin.x.clone() + } + + /// Returns the right edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the right edge of the bounds. + pub fn right(&self) -> T { + self.origin.x.clone() + self.size.width.clone() + } + + /// Returns the top right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the top right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let top_right = bounds.top_right(); + /// assert_eq!(top_right, Point { x: 10, y: 0 }); + /// ``` + pub fn top_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone(), + } + } + + /// Returns the bottom right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the bottom right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_right = bounds.bottom_right(); + /// assert_eq!(bottom_right, Point { x: 10, y: 20 }); + /// ``` + pub fn bottom_right(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } + + /// Returns the bottom left corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the bottom left corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_left = bounds.bottom_left(); + /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); + /// ``` + pub fn bottom_left(&self) -> Point { + Point { + x: self.origin.x.clone(), + y: self.origin.y.clone() + self.size.height.clone(), + } + } +} + +impl Bounds +where + T: Add + Half + Clone + Debug + Default + PartialEq, +{ + /// Returns the requested corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the corner of the bounds requested by the parameter. + /// + /// # Examples + /// + /// ``` + /// use gpui::{Bounds, Anchor, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let bottom_left = bounds.corner(Anchor::BottomLeft); + /// assert_eq!(bottom_left, Point { x: 0, y: 20 }); + /// ``` + pub fn corner(&self, corner: Anchor) -> Point { + match corner { + Anchor::TopLeft => self.origin.clone(), + Anchor::TopRight => self.top_right(), + Anchor::BottomLeft => self.bottom_left(), + Anchor::BottomRight => self.bottom_right(), + Anchor::TopCenter => self.top_center(), + Anchor::BottomCenter => self.bottom_center(), + Anchor::LeftCenter => self.left_center(), + Anchor::RightCenter => self.right_center(), + } + } +} + +impl Bounds +where + T: Add + PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Checks if the given point is within the bounds. + /// + /// This method determines whether a point lies inside the rectangle defined by the bounds, + /// including the edges. The point is considered inside if its x-coordinate is greater than + /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate + /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. + /// + /// # Arguments + /// + /// * `point` - A reference to a `Point` that represents the point to check. + /// + /// # Returns + /// + /// Returns `true` if the point is within the bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Point, Bounds, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let inside_point = Point { x: 5, y: 5 }; + /// let outside_point = Point { x: 15, y: 15 }; + /// + /// assert!(bounds.contains(&inside_point)); + /// assert!(!bounds.contains(&outside_point)); + /// ``` + pub fn contains(&self, point: &Point) -> bool { + point.x >= self.origin.x + && point.x < self.origin.x.clone() + self.size.width.clone() + && point.y >= self.origin.y + && point.y < self.origin.y.clone() + self.size.height.clone() + } + + /// Checks if this bounds is completely contained within another bounds. + /// + /// This method determines whether the current bounds is entirely enclosed by the given bounds. + /// A bounds is considered to be contained within another if its origin (top-left corner) and + /// its bottom-right corner are both contained within the other bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` that might contain this bounds. + /// + /// # Returns + /// + /// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let outer_bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }; + /// let inner_bounds = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let overlapping_bounds = Bounds { + /// origin: Point { x: 15, y: 15 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert!(inner_bounds.is_contained_within(&outer_bounds)); + /// assert!(!overlapping_bounds.is_contained_within(&outer_bounds)); + /// ``` + pub fn is_contained_within(&self, other: &Self) -> bool { + other.contains(&self.origin) && other.contains(&self.bottom_right()) + } + + /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. + /// + /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to the `origin` and + /// `size` fields, resulting in new bounds of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Bounds` with the origin and size mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map(|value| value as f64 * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map(&self, f: impl Fn(T) -> U) -> Bounds + where + U: Clone + Debug + Default + PartialEq, + { + Bounds { + origin: self.origin.map(&f), + size: self.size.map(f), + } + } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_origin(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }); + /// ``` + pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { + Bounds { + origin: self.origin.map(f), + size: self.size, + } + } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_size(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds { + Bounds { + origin: self.origin, + size: self.size.map(f), + } + } +} + +impl Bounds +where + T: Add + Sub + PartialOrd + Clone + Debug + Default + PartialEq, +{ + /// Convert a point to the coordinate space defined by this Bounds + pub fn localize(&self, point: &Point) -> Option> { + self.contains(point) + .then(|| point.relative_to(&self.origin)) + } +} + +/// Checks if the bounds represent an empty area. +/// +/// # Returns +/// +/// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. +impl Bounds { + /// Checks if the bounds represent an empty area. + /// + /// # Returns + /// + /// Returns `true` if either the width or the height of the bounds is less than or equal to zero, indicating an empty area. + #[must_use] + pub fn is_empty(&self) -> bool { + self.size.width <= T::default() || self.size.height <= T::default() + } +} + +impl> Display for Bounds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} - {} (size {})", + self.origin, + self.bottom_right(), + self.size + ) + } +} + +impl Size { + /// Converts the size from physical to logical pixels. + pub fn to_pixels(self, scale_factor: f32) -> Size { + size( + px(self.width.0 as f32 / scale_factor), + px(self.height.0 as f32 / scale_factor), + ) + } +} + +impl Size { + /// Converts the size from logical to physical pixels. + pub fn to_device_pixels(self, scale_factor: f32) -> Size { + size( + DevicePixels((self.width.0 * scale_factor).round() as i32), + DevicePixels((self.height.0 * scale_factor).round() as i32), + ) + } +} + +impl Bounds { + /// Scales the bounds by a given factor, typically used to adjust for display scaling. + /// + /// This method multiplies the origin and size of the bounds by the provided scaling factor, + /// resulting in a new `Bounds` that is proportionally larger or smaller + /// depending on the scaling factor. This can be used to ensure that the bounds are properly + /// scaled for different display densities. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. + /// + /// # Returns + /// + /// Returns a new `Bounds` that represents the scaled bounds. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Bounds, Point, Size, Pixels, ScaledPixels, DevicePixels}; + /// let bounds = Bounds { + /// origin: Point { x: Pixels::from(10.0), y: Pixels::from(20.0) }, + /// size: Size { width: Pixels::from(30.0), height: Pixels::from(40.0) }, + /// }; + /// let display_scale_factor = 2.0; + /// let scaled_bounds = bounds.scale(display_scale_factor); + /// assert_eq!(scaled_bounds, Bounds { + /// origin: Point { + /// x: ScaledPixels::from(20.0), + /// y: ScaledPixels::from(40.0), + /// }, + /// size: Size { + /// width: ScaledPixels::from(60.0), + /// height: ScaledPixels::from(80.0) + /// }, + /// }); + /// ``` + pub fn scale(&self, factor: f32) -> Bounds { + Bounds { + origin: self.origin.scale(factor), + size: self.size.scale(factor), + } + } + + /// Convert the bounds from logical pixels to physical pixels + pub fn to_device_pixels(self, factor: f32) -> Bounds { + Bounds { + origin: point( + DevicePixels((self.origin.x.0 * factor).round() as i32), + DevicePixels((self.origin.y.0 * factor).round() as i32), + ), + size: self.size.to_device_pixels(factor), + } + } +} + +impl Bounds { + /// Convert the bounds from physical pixels to logical pixels + pub fn to_pixels(self, scale_factor: f32) -> Bounds { + Bounds { + origin: point( + px(self.origin.x.0 as f32 / scale_factor), + px(self.origin.y.0 as f32 / scale_factor), + ), + size: self.size.to_pixels(scale_factor), + } + } +} + +/// Represents the edges of a box in a 2D space, such as padding or margin. +/// +/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. +/// +/// # Examples +/// +/// ``` +/// # use gpui::Edges; +/// let edges = Edges { +/// top: 10.0, +/// right: 20.0, +/// bottom: 30.0, +/// left: 40.0, +/// }; +/// +/// assert_eq!(edges.top, 10.0); +/// assert_eq!(edges.right, 20.0); +/// assert_eq!(edges.bottom, 30.0); +/// assert_eq!(edges.left, 40.0); +/// ``` +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Edges { + /// The size of the top edge. + pub top: T, + /// The size of the right edge. + pub right: T, + /// The size of the bottom edge. + pub bottom: T, + /// The size of the left edge. + pub left: T, +} + +impl Mul for Edges +where + T: Mul + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top: self.top.clone() * rhs.top, + right: self.right.clone() * rhs.right, + bottom: self.bottom.clone() * rhs.bottom, + left: self.left * rhs.left, + } + } +} + +impl MulAssign for Edges +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top = self.top.clone() * rhs.clone(); + self.right = self.right.clone() * rhs.clone(); + self.bottom = self.bottom.clone() * rhs.clone(); + self.left = self.left.clone() * rhs; + } +} + +impl Copy for Edges {} + +impl Edges { + /// Constructs `Edges` where all sides are set to the same specified value. + /// + /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, + /// such as padding or margin with the same size on all sides. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four sides of the edges. + /// + /// # Returns + /// + /// An `Edges` instance with all sides set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let uniform_edges = Edges::all(10.0); + /// assert_eq!(uniform_edges.top, 10.0); + /// assert_eq!(uniform_edges.right, 10.0); + /// assert_eq!(uniform_edges.bottom, 10.0); + /// assert_eq!(uniform_edges.left, 10.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top: value.clone(), + right: value.clone(), + bottom: value.clone(), + left: value, + } + } + + /// Applies a function to each field of the `Edges`, producing a new `Edges`. + /// + /// This method allows for converting an `Edges` to an `Edges` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Edges` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; + /// let edges_float = edges.map(|&value| value as f32 * 1.1); + /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); + /// ``` + pub fn map(&self, f: impl Fn(&T) -> U) -> Edges + where + U: Clone + Debug + Default + PartialEq, + { + Edges { + top: f(&self.top), + right: f(&self.right), + bottom: f(&self.bottom), + left: f(&self.left), + } + } + + /// Checks if any of the edges satisfy a given predicate. + /// + /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. + /// + /// # Arguments + /// + /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. + /// + /// # Returns + /// + /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Edges; + /// let edges = Edges { + /// top: 10, + /// right: 0, + /// bottom: 5, + /// left: 0, + /// }; + /// + /// assert!(edges.any(|value| *value == 0)); + /// assert!(edges.any(|value| *value > 0)); + /// assert!(!edges.any(|value| *value > 10)); + /// ``` + pub fn any bool>(&self, predicate: F) -> bool { + predicate(&self.top) + || predicate(&self.right) + || predicate(&self.bottom) + || predicate(&self.left) + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. + /// + /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to `Length::Auto`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, Length}; + /// let auto_edges = Edges::auto(); + /// assert_eq!(auto_edges.top, Length::Auto); + /// assert_eq!(auto_edges.right, Length::Auto); + /// assert_eq!(auto_edges.bottom, Length::Auto); + /// assert_eq!(auto_edges.left, Length::Auto); + /// ``` + pub fn auto() -> Self { + Self { + top: Length::Auto, + right: Length::Auto, + bottom: Length::Auto, + left: Length::Auto, + } + } + + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{DefiniteLength, Edges, Length, Pixels}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels::ZERO))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{px, DefiniteLength, Edges}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.right, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.bottom, DefiniteLength::from(px(0.))); + /// assert_eq!(no_edges.left, DefiniteLength::from(px(0.))); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. + /// + /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account + /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). + /// + /// # Arguments + /// + /// * `parent_size` - `Size` representing the size of the parent element. + /// * `rem_size` - `Pixels` representing the size of one REM unit. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, DefiniteLength, px, AbsoluteLength, rems, Size}; + /// let edges = Edges { + /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), + /// right: DefiniteLength::Fraction(0.5), + /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), + /// left: DefiniteLength::Fraction(0.25), + /// }; + /// let parent_size = Size { + /// width: AbsoluteLength::Pixels(px(200.0)), + /// height: AbsoluteLength::Pixels(px(100.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels + /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width + /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems + /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width + /// ``` + pub fn to_pixels(self, parent_size: Size, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(parent_size.height, rem_size), + right: self.right.to_pixels(parent_size.width, rem_size), + bottom: self.bottom.to_pixels(parent_size.height, rem_size), + left: self.left.to_pixels(parent_size.width, rem_size), + } + } +} + +impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{AbsoluteLength, Edges, Pixels}; + /// let no_edges = Edges::::zero(); + /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels::ZERO)); + /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels::ZERO)); + /// ``` + pub fn zero() -> Self { + Self { + top: px(0.).into(), + right: px(0.).into(), + bottom: px(0.).into(), + left: px(0.).into(), + } + } + + /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. + /// + /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. + /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem unit in pixels. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, AbsoluteLength, Pixels, px, rems}; + /// let edges = Edges { + /// top: AbsoluteLength::Pixels(px(10.0)), + /// right: AbsoluteLength::Rems(rems(1.0)), + /// bottom: AbsoluteLength::Pixels(px(20.0)), + /// left: AbsoluteLength::Rems(rems(2.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels + /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Edges { + Edges { + top: self.top.to_pixels(rem_size), + right: self.right.to_pixels(rem_size), + bottom: self.bottom.to_pixels(rem_size), + left: self.left.to_pixels(rem_size), + } + } +} + +impl Edges { + /// Scales the `Edges` by a given factor, returning `Edges`. + /// + /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each edge. + /// + /// # Returns + /// + /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Edges, Pixels, ScaledPixels}; + /// let edges = Edges { + /// top: Pixels::from(10.0), + /// right: Pixels::from(20.0), + /// bottom: Pixels::from(30.0), + /// left: Pixels::from(40.0), + /// }; + /// let scaled_edges = edges.scale(2.0); + /// assert_eq!(scaled_edges.top, ScaledPixels::from(20.0)); + /// assert_eq!(scaled_edges.right, ScaledPixels::from(40.0)); + /// assert_eq!(scaled_edges.bottom, ScaledPixels::from(60.0)); + /// assert_eq!(scaled_edges.left, ScaledPixels::from(80.0)); + /// ``` + pub fn scale(&self, factor: f32) -> Edges { + Edges { + top: self.top.scale(factor), + right: self.right.scale(factor), + bottom: self.bottom.scale(factor), + left: self.left.scale(factor), + } + } + + /// Returns the maximum value of any edge. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four edges. + pub fn max(&self) -> Pixels { + self.top.max(self.right).max(self.bottom).max(self.left) + } +} + +impl From for Edges { + fn from(val: f32) -> Self { + let val: Pixels = val.into(); + val.into() + } +} + +impl From for Edges { + fn from(val: Pixels) -> Self { + Edges { + top: val, + right: val, + bottom: val, + left: val, + } + } +} + +/// Identifies a reference point on a 2D box, used to anchor positioned elements. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Anchor { + /// The top left corner + TopLeft, + /// The top right corner + TopRight, + /// The bottom left corner + BottomLeft, + /// The bottom right corner + BottomRight, + /// The top center position + TopCenter, + /// The bottom center position + BottomCenter, + /// The left center position + LeftCenter, + /// The right center position + RightCenter, +} + +impl Anchor { + /// Returns the directly opposite corner. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Anchor; + /// assert_eq!(Anchor::TopLeft.opposite(), Anchor::BottomRight); + /// ``` + #[must_use] + pub fn opposite(self) -> Self { + match self { + Anchor::TopLeft => Anchor::BottomRight, + Anchor::TopRight => Anchor::BottomLeft, + Anchor::BottomLeft => Anchor::TopRight, + Anchor::BottomRight => Anchor::TopLeft, + Anchor::TopCenter => Anchor::BottomCenter, + Anchor::BottomCenter => Anchor::TopCenter, + Anchor::LeftCenter => Anchor::RightCenter, + Anchor::RightCenter => Anchor::LeftCenter, + } + } + + /// Returns the corner across from this corner, moving along the specified axis. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Axis, Anchor}; + /// let result = Anchor::TopLeft.other_side_along(Axis::Horizontal); + /// assert_eq!(result, Anchor::TopRight); + /// ``` + #[must_use] + pub fn other_side_along(self, axis: Axis) -> Self { + match axis { + Axis::Vertical => match self { + Anchor::TopLeft => Anchor::BottomLeft, + Anchor::TopRight => Anchor::BottomRight, + Anchor::BottomLeft => Anchor::TopLeft, + Anchor::BottomRight => Anchor::TopRight, + Anchor::TopCenter => Anchor::BottomCenter, + Anchor::BottomCenter => Anchor::TopCenter, + Anchor::LeftCenter => Anchor::LeftCenter, + Anchor::RightCenter => Anchor::RightCenter, + }, + Axis::Horizontal => match self { + Anchor::TopLeft => Anchor::TopRight, + Anchor::TopRight => Anchor::TopLeft, + Anchor::BottomLeft => Anchor::BottomRight, + Anchor::BottomRight => Anchor::BottomLeft, + Anchor::TopCenter => Anchor::TopCenter, + Anchor::BottomCenter => Anchor::BottomCenter, + Anchor::LeftCenter => Anchor::RightCenter, + Anchor::RightCenter => Anchor::LeftCenter, + }, + } + } + + /// Returns whether the anchor is center-positioned. + #[inline] + pub fn is_center(&self) -> bool { + matches!( + self, + Self::TopCenter | Self::BottomCenter | Self::LeftCenter | Self::RightCenter + ) + } + + /// Returns whether the anchor is bottom-positioned. + #[inline] + pub fn is_bottom(&self) -> bool { + matches!( + self, + Self::BottomCenter | Self::BottomLeft | Self::BottomRight + ) + } +} + +/// Represents the corners of a box in a 2D space, such as border radius. +/// +/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. +#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub struct Corners { + /// The value associated with the top left corner. + pub top_left: T, + /// The value associated with the top right corner. + pub top_right: T, + /// The value associated with the bottom right corner. + pub bottom_right: T, + /// The value associated with the bottom left corner. + pub bottom_left: T, +} + +impl Corners +where + T: Add + Half + Clone + Debug + Default + PartialEq, +{ + /// Constructs `Corners` where all sides are set to the same specified value. + /// + /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, + /// such as a uniform border radius on a rectangle. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four corners. + /// + /// # Returns + /// + /// An `Corners` instance with all corners set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use gpui::Corners; + /// let uniform_corners = Corners::all(5.0); + /// assert_eq!(uniform_corners.top_left, 5.0); + /// assert_eq!(uniform_corners.top_right, 5.0); + /// assert_eq!(uniform_corners.bottom_right, 5.0); + /// assert_eq!(uniform_corners.bottom_left, 5.0); + /// ``` + pub fn all(value: T) -> Self { + Self { + top_left: value.clone(), + top_right: value.clone(), + bottom_right: value.clone(), + bottom_left: value, + } + } + + /// Returns the requested corner value, supporting all eight corner positions. + /// + /// For the four basic corners (TopLeft, TopRight, BottomLeft, BottomRight), + /// this returns the corresponding field value directly. + /// + /// For the center positions (TopCenter, BottomCenter, LeftCenter, RightCenter), + /// this calculates the average of the two adjacent corners. + /// + /// # Returns + /// + /// A value of type `T` representing the corner requested by the parameter. + /// + /// # Examples + /// + /// Basic corner positions: + /// + /// ``` + /// # use gpui::{Anchor, Corners}; + /// let corners = Corners { + /// top_left: 10, + /// top_right: 20, + /// bottom_left: 30, + /// bottom_right: 40 + /// }; + /// assert_eq!(corners.corner(Anchor::TopLeft), 10); + /// assert_eq!(corners.corner(Anchor::BottomRight), 40); + /// ``` + /// + /// Center positions (calculated as average of adjacent corners): + /// + /// ``` + /// # use gpui::{Anchor, Corners}; + /// let corners = Corners { + /// top_left: 10, + /// top_right: 20, + /// bottom_left: 30, + /// bottom_right: 40 + /// }; + /// assert_eq!(corners.corner(Anchor::TopCenter), 15); + /// assert_eq!(corners.corner(Anchor::BottomCenter), 35); + /// assert_eq!(corners.corner(Anchor::LeftCenter), 20); + /// assert_eq!(corners.corner(Anchor::RightCenter), 30); + /// ``` + #[must_use] + pub fn corner(&self, corner: Anchor) -> T { + match corner { + Anchor::TopLeft => self.top_left.clone(), + Anchor::TopRight => self.top_right.clone(), + Anchor::BottomLeft => self.bottom_left.clone(), + Anchor::BottomRight => self.bottom_right.clone(), + Anchor::TopCenter => (self.top_left.clone() + self.top_right.clone()).half(), + Anchor::BottomCenter => (self.bottom_left.clone() + self.bottom_right.clone()).half(), + Anchor::LeftCenter => (self.top_left.clone() + self.bottom_left.clone()).half(), + Anchor::RightCenter => (self.top_right.clone() + self.bottom_right.clone()).half(), + } + } +} + +impl Corners { + /// Converts the `AbsoluteLength` to `Pixels` based on the provided rem size. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. + /// + /// # Returns + /// + /// Returns a `Corners` instance with each corner's length converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, AbsoluteLength, Pixels, Rems, Size}; + /// let corners = Corners { + /// top_left: AbsoluteLength::Pixels(Pixels::from(15.0)), + /// top_right: AbsoluteLength::Rems(Rems(1.0)), + /// bottom_right: AbsoluteLength::Pixels(Pixels::from(30.0)), + /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), + /// }; + /// let rem_size = Pixels::from(16.0); + /// let corners_in_pixels = corners.to_pixels(rem_size); + /// + /// assert_eq!(corners_in_pixels.top_left, Pixels::from(15.0)); + /// assert_eq!(corners_in_pixels.top_right, Pixels::from(16.0)); // 1 rem converted to pixels + /// assert_eq!(corners_in_pixels.bottom_right, Pixels::from(30.0)); + /// assert_eq!(corners_in_pixels.bottom_left, Pixels::from(32.0)); // 2 rems converted to pixels + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Corners { + Corners { + top_left: self.top_left.to_pixels(rem_size), + top_right: self.top_right.to_pixels(rem_size), + bottom_right: self.bottom_right.to_pixels(rem_size), + bottom_left: self.bottom_left.to_pixels(rem_size), + } + } +} + +impl Corners { + /// Scales the `Corners` by a given factor, returning `Corners`. + /// + /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each corner. + /// + /// # Returns + /// + /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, Pixels, ScaledPixels}; + /// let corners = Corners { + /// top_left: Pixels::from(10.0), + /// top_right: Pixels::from(20.0), + /// bottom_right: Pixels::from(30.0), + /// bottom_left: Pixels::from(40.0), + /// }; + /// let scaled_corners = corners.scale(2.0); + /// assert_eq!(scaled_corners.top_left, ScaledPixels::from(20.0)); + /// assert_eq!(scaled_corners.top_right, ScaledPixels::from(40.0)); + /// assert_eq!(scaled_corners.bottom_right, ScaledPixels::from(60.0)); + /// assert_eq!(scaled_corners.bottom_left, ScaledPixels::from(80.0)); + /// ``` + #[must_use] + pub fn scale(&self, factor: f32) -> Corners { + Corners { + top_left: self.top_left.scale(factor), + top_right: self.top_right.scale(factor), + bottom_right: self.bottom_right.scale(factor), + bottom_left: self.bottom_left.scale(factor), + } + } + + /// Returns the maximum value of any corner. + /// + /// # Returns + /// + /// The maximum `Pixels` value among all four corners. + #[must_use] + pub fn max(&self) -> Pixels { + self.top_left + .max(self.top_right) + .max(self.bottom_right) + .max(self.bottom_left) + } +} + +impl + Ord + Clone + Debug + Default + PartialEq> Corners { + /// Clamps corner radii to be less than or equal to half the shortest side of a quad. + /// + /// # Arguments + /// + /// * `size` - The size of the quad which limits the size of the corner radii. + /// + /// # Returns + /// + /// Anchor radii values clamped to fit. + #[must_use] + pub fn clamp_radii_for_quad_size(self, size: Size) -> Corners { + let max = cmp::min(size.width, size.height) / 2.; + Corners { + top_left: cmp::min(self.top_left, max.clone()), + top_right: cmp::min(self.top_right, max.clone()), + bottom_right: cmp::min(self.bottom_right, max.clone()), + bottom_left: cmp::min(self.bottom_left, max), + } + } +} + +impl Corners { + /// Applies a function to each field of the `Corners`, producing a new `Corners`. + /// + /// This method allows for converting a `Corners` to a `Corners` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Corners` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{Corners, Pixels, Rems}; + /// let corners = Corners { + /// top_left: Pixels::from(10.0), + /// top_right: Pixels::from(20.0), + /// bottom_right: Pixels::from(30.0), + /// bottom_left: Pixels::from(40.0), + /// }; + /// let corners_in_rems = corners.map(|&px| Rems(f32::from(px) / 16.0)); + /// assert_eq!(corners_in_rems, Corners { + /// top_left: Rems(0.625), + /// top_right: Rems(1.25), + /// bottom_right: Rems(1.875), + /// bottom_left: Rems(2.5), + /// }); + /// ``` + #[must_use] + pub fn map(&self, f: impl Fn(&T) -> U) -> Corners + where + U: Clone + Debug + Default + PartialEq, + { + Corners { + top_left: f(&self.top_left), + top_right: f(&self.top_right), + bottom_right: f(&self.bottom_right), + bottom_left: f(&self.bottom_left), + } + } +} + +impl Mul for Corners +where + T: Mul + Clone + Debug + Default + PartialEq, +{ + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + top_left: self.top_left.clone() * rhs.top_left, + top_right: self.top_right.clone() * rhs.top_right, + bottom_right: self.bottom_right.clone() * rhs.bottom_right, + bottom_left: self.bottom_left * rhs.bottom_left, + } + } +} + +impl MulAssign for Corners +where + T: Mul + Clone + Debug + Default + PartialEq, + S: Clone, +{ + fn mul_assign(&mut self, rhs: S) { + self.top_left = self.top_left.clone() * rhs.clone(); + self.top_right = self.top_right.clone() * rhs.clone(); + self.bottom_right = self.bottom_right.clone() * rhs.clone(); + self.bottom_left = self.bottom_left.clone() * rhs; + } +} + +impl Copy for Corners where T: Copy + Clone + Debug + Default + PartialEq {} + +impl From for Corners { + fn from(val: f32) -> Self { + Corners { + top_left: val.into(), + top_right: val.into(), + bottom_right: val.into(), + bottom_left: val.into(), + } + } +} + +impl From for Corners { + fn from(val: Pixels) -> Self { + Corners { + top_left: val, + top_right: val, + bottom_right: val, + bottom_left: val, + } + } +} + +/// Represents an angle in Radians +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Radians(pub f32); + +/// Create a `Radian` from a raw value +pub fn radians(value: f32) -> Radians { + Radians(value) +} + +/// A type representing a percentage value. +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + Debug, +)] +#[repr(transparent)] +pub struct Percentage(pub f32); + +/// Generate a `Radian` from a percentage of a full circle. +pub fn percentage(value: f32) -> Percentage { + debug_assert!( + (0.0..=1.0).contains(&value), + "Percentage must be between 0 and 1" + ); + Percentage(value) +} + +impl From for Radians { + fn from(value: Percentage) -> Self { + radians(value.0 * std::f32::consts::PI * 2.0) + } +} + +/// Represents a length in pixels, the base unit of measurement in the UI framework. +/// +/// `Pixels` is a value type that represents an absolute length in pixels, which is used +/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit +/// of measurement for all visual elements and layout calculations. +/// +/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for +/// anti-aliasing and animations. However, when applied to actual pixel grids, the value +/// is typically rounded to the nearest integer. +/// +/// # Examples +/// +/// ``` +/// use gpui::{Pixels, ScaledPixels}; +/// +/// // Define a length of 10 pixels +/// let length = Pixels::from(10.0); +/// +/// // Define a length and scale it by a factor of 2 +/// let scaled_length = length.scale(2.0); +/// assert_eq!(scaled_length, ScaledPixels::from(20.0)); +/// ``` +#[derive( + Clone, + Copy, + Default, + Add, + AddAssign, + Sub, + SubAssign, + Neg, + Div, + DivAssign, + PartialEq, + Serialize, + Deserialize, + JsonSchema, +)] +#[repr(transparent)] +pub struct Pixels(pub(crate) f32); + +impl Div for Pixels { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl std::ops::DivAssign for Pixels { + fn div_assign(&mut self, rhs: Self) { + *self = Self(self.0 / rhs.0); + } +} + +impl std::ops::RemAssign for Pixels { + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } +} + +impl std::ops::Rem for Pixels { + type Output = Self; + + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } +} + +impl Mul for Pixels { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } +} + +impl Mul for f32 { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Self::Output { + rhs * self + } +} + +impl Mul for Pixels { + type Output = Self; + + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } +} + +impl Mul for usize { + type Output = Pixels; + + fn mul(self, rhs: Pixels) -> Pixels { + rhs * self + } +} + +impl MulAssign for Pixels { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs; + } +} + +impl Display for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}px", self.0) + } +} + +impl Debug for Pixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl std::iter::Sum for Pixels { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |a, b| a + b) + } +} + +impl<'a> std::iter::Sum<&'a Pixels> for Pixels { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |a, b| a + *b) + } +} + +impl TryFrom<&'_ str> for Pixels { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + value + .strip_suffix("px") + .context("expected 'px' suffix") + .and_then(|number| Ok(number.parse()?)) + .map(Self) + } +} + +impl Pixels { + /// Represents zero pixels. + pub const ZERO: Pixels = Pixels(0.0); + /// The maximum value that can be represented by `Pixels`. + pub const MAX: Pixels = Pixels(f32::MAX); + /// The minimum value that can be represented by `Pixels`. + pub const MIN: Pixels = Pixels(f32::MIN); + + /// Returns the raw `f32` value of this `Pixels`. + pub fn as_f32(self) -> f32 { + self.0 + } + + /// Floors the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the rounded value. + pub fn round(&self) -> Self { + Self(self.0.round()) + } + + /// Returns the ceiling of the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the ceiling value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } + + /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. + /// + /// This method is used when adjusting pixel values for display scaling factors, + /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and + /// thus requires scaling to maintain visual consistency and readability. + /// + /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering + /// calculations where display scaling is considered. + #[must_use] + pub fn scale(&self, factor: f32) -> ScaledPixels { + ScaledPixels(self.0 * factor) + } + + /// Raises the `Pixels` value to a given power. + /// + /// # Arguments + /// + /// * `exponent` - The exponent to raise the `Pixels` value by. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the value raised to the given exponent. + pub fn pow(&self, exponent: f32) -> Self { + Self(self.0.powf(exponent)) + } + + /// Returns the absolute value of the `Pixels`. + /// + /// # Returns + /// + /// A new `Pixels` instance with the absolute value of the original `Pixels`. + pub fn abs(&self) -> Self { + Self(self.0.abs()) + } + + /// Returns the sign of the `Pixels` value. + /// + /// # Returns + /// + /// Returns: + /// * `1.0` if the value is positive + /// * `-1.0` if the value is negative + pub fn signum(&self) -> f32 { + self.0.signum() + } + + /// Returns the f64 value of `Pixels`. + /// + /// # Returns + /// + /// A f64 value of the `Pixels`. + pub fn to_f64(self) -> f64 { + self.0 as f64 + } +} + +impl Eq for Pixels {} + +impl PartialOrd for Pixels { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Pixels { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.total_cmp(&other.0) + } +} + +impl std::hash::Hash for Pixels { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl From for Pixels { + fn from(pixels: f64) -> Self { + Pixels(pixels as f32) + } +} + +impl From for Pixels { + fn from(pixels: f32) -> Self { + Pixels(pixels) + } +} + +impl From for f32 { + fn from(pixels: Pixels) -> Self { + pixels.0 + } +} + +impl From<&Pixels> for f32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 + } +} + +impl From for f64 { + fn from(pixels: Pixels) -> Self { + pixels.0 as f64 + } +} + +impl From for u32 { + fn from(pixels: Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From<&Pixels> for u32 { + fn from(pixels: &Pixels) -> Self { + pixels.0 as u32 + } +} + +impl From for Pixels { + fn from(pixels: u32) -> Self { + Pixels(pixels as f32) + } +} + +impl From for usize { + fn from(pixels: Pixels) -> Self { + pixels.0 as usize + } +} + +impl From for Pixels { + fn from(pixels: usize) -> Self { + Pixels(pixels as f32) + } +} + +/// Represents physical pixels on the display. +/// +/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. +/// This type is used when precise pixel manipulation is required, such as rendering graphics or +/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be +/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the +/// display. +#[derive( + Add, + AddAssign, + Clone, + Copy, + Default, + Div, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + Sub, + SubAssign, + Serialize, + Deserialize, +)] +#[repr(transparent)] +pub struct DevicePixels(pub i32); + +impl DevicePixels { + /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. + /// + /// This function is useful when working with graphical data that needs to be stored in a buffer, + /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. + /// + /// # Arguments + /// + /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. + /// + /// # Returns + /// + /// The number of bytes required to represent the `DevicePixels` value in memory. + /// + /// # Examples + /// + /// ``` + /// # use gpui::DevicePixels; + /// let pixels = DevicePixels(10); // 10 device pixels + /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) + /// let total_bytes = pixels.to_bytes(bytes_per_pixel); + /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes + /// ``` + pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 { + self.0 as u32 * bytes_per_pixel as u32 + } +} + +impl fmt::Debug for DevicePixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} px (device)", self.0) + } +} + +impl From for i32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 + } +} + +impl From for DevicePixels { + fn from(device_pixels: i32) -> Self { + DevicePixels(device_pixels) + } +} + +impl From for DevicePixels { + fn from(device_pixels: u32) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for u32 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u32 + } +} + +impl From for u64 { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as u64 + } +} + +impl From for DevicePixels { + fn from(device_pixels: u64) -> Self { + DevicePixels(device_pixels as i32) + } +} + +impl From for usize { + fn from(device_pixels: DevicePixels) -> Self { + device_pixels.0 as usize + } +} + +impl From for DevicePixels { + fn from(device_pixels: usize) -> Self { + DevicePixels(device_pixels as i32) + } +} + +/// Represents scaled pixels that take into account the device's scale factor. +/// +/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices +/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), +/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, +/// dimensions and positions can be specified in a way that scales appropriately across different +/// display resolutions. +#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, DivAssign, PartialEq)] +#[repr(transparent)] +pub struct ScaledPixels(pub f32); + +impl ScaledPixels { + /// Returns the raw `f32` value of this `ScaledPixels`. + pub fn as_f32(self) -> f32 { + self.0 + } + + /// Floors the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the floored value. + pub fn floor(&self) -> Self { + Self(self.0.floor()) + } + + /// Rounds the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the rounded value. + pub fn round(&self) -> Self { + Self(self.0.round()) + } + + /// Ceils the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the ceiled value. + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } +} + +impl Eq for ScaledPixels {} + +impl PartialOrd for ScaledPixels { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScaledPixels { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.0.total_cmp(&other.0) + } +} + +impl Debug for ScaledPixels { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}px (scaled)", self.0) + } +} + +impl From for DevicePixels { + fn from(scaled: ScaledPixels) -> Self { + DevicePixels(scaled.0.ceil() as i32) + } +} + +impl From for ScaledPixels { + fn from(device: DevicePixels) -> Self { + ScaledPixels(device.0 as f32) + } +} + +impl From for f64 { + fn from(scaled_pixels: ScaledPixels) -> Self { + scaled_pixels.0 as f64 + } +} + +impl From for u32 { + fn from(pixels: ScaledPixels) -> Self { + pixels.0 as u32 + } +} + +impl From for ScaledPixels { + fn from(pixels: f32) -> Self { + Self(pixels) + } +} + +impl Div for ScaledPixels { + type Output = f32; + + fn div(self, rhs: Self) -> Self::Output { + self.0 / rhs.0 + } +} + +impl std::ops::DivAssign for ScaledPixels { + fn div_assign(&mut self, rhs: Self) { + *self = Self(self.0 / rhs.0); + } +} + +impl std::ops::RemAssign for ScaledPixels { + fn rem_assign(&mut self, rhs: Self) { + self.0 %= rhs.0; + } +} + +impl std::ops::Rem for ScaledPixels { + type Output = Self; + + fn rem(self, rhs: Self) -> Self { + Self(self.0 % rhs.0) + } +} + +impl Mul for ScaledPixels { + type Output = Self; + + fn mul(self, rhs: f32) -> Self { + Self(self.0 * rhs) + } +} + +impl Mul for f32 { + type Output = ScaledPixels; + + fn mul(self, rhs: ScaledPixels) -> Self::Output { + rhs * self + } +} + +impl Mul for ScaledPixels { + type Output = Self; + + fn mul(self, rhs: usize) -> Self { + self * (rhs as f32) + } +} + +impl Mul for usize { + type Output = ScaledPixels; + + fn mul(self, rhs: ScaledPixels) -> ScaledPixels { + rhs * self + } +} + +impl MulAssign for ScaledPixels { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs; + } +} + +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`Window::set_rem_size`][set_rem_size]. +/// +/// Rems are used for defining lengths that are scalable and consistent across different UI elements. +/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), +/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar +/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. +/// +/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. +/// +/// [set_rem_size]: crate::Window::set_rem_size +#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)] +pub struct Rems(pub f32); + +impl Rems { + /// A length of zero. + pub const ZERO: Self = Self(0.0); + /// Convert this Rem value to pixels. + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + self * rem_size + } + /// Convert from pixels to Rem + pub fn from_pixels(length: Pixels, window: &gpui::Window) -> Self { + Self(length / window.rem_size()) + } +} + +impl Mul for Rems { + type Output = Pixels; + + fn mul(self, other: Pixels) -> Pixels { + Pixels(self.0 * other.0) + } +} + +impl AddAssign for Rems { + fn add_assign(&mut self, rhs: Rems) { + self.0 += rhs.0 + } +} + +impl Display for Rems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}rem", self.0) + } +} + +impl Debug for Rems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl TryFrom<&'_ str> for Rems { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + value + .strip_suffix("rem") + .context("expected 'rem' suffix") + .and_then(|number| Ok(number.parse()?)) + .map(Self) + } +} + +/// Represents an absolute length in pixels or rems. +/// +/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not +/// affected by the current font size, or a number of rems, which is relative to the font size of +/// the root element. It is used for specifying dimensions that are either independent of or +/// related to the typographic scale. +#[derive(Clone, Copy, Neg, PartialEq)] +pub enum AbsoluteLength { + /// A length in pixels. + Pixels(Pixels), + /// A length in rems. + Rems(Rems), +} + +impl AbsoluteLength { + /// Checks if the absolute length is zero. + pub fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(px) => px.0 == 0.0, + AbsoluteLength::Rems(rems) => rems.0 == 0.0, + } + } +} + +impl From for AbsoluteLength { + fn from(pixels: Pixels) -> Self { + AbsoluteLength::Pixels(pixels) + } +} + +impl From for AbsoluteLength { + fn from(rems: Rems) -> Self { + AbsoluteLength::Rems(rems) + } +} + +impl AbsoluteLength { + /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{AbsoluteLength, Pixels, Rems}; + /// let length_in_pixels = AbsoluteLength::Pixels(Pixels::from(42.0)); + /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); + /// let rem_size = Pixels::from(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels::from(42.0)); + /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels::from(32.0)); + /// ``` + pub fn to_pixels(self, rem_size: Pixels) -> Pixels { + match self { + AbsoluteLength::Pixels(pixels) => pixels, + AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size), + } + } + + /// Converts an `AbsoluteLength` to `Rems` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + pub fn to_rems(self, rem_size: Pixels) -> Rems { + match self { + AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0), + AbsoluteLength::Rems(rems) => rems, + } + } +} + +impl Default for AbsoluteLength { + fn default() -> Self { + px(0.).into() + } +} + +impl Display for AbsoluteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pixels(pixels) => write!(f, "{pixels}"), + Self::Rems(rems) => write!(f, "{rems}"), + } + } +} + +impl Debug for AbsoluteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +const EXPECTED_ABSOLUTE_LENGTH: &str = "number with 'px' or 'rem' suffix"; + +impl TryFrom<&'_ str> for AbsoluteLength { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if let Ok(pixels) = value.try_into() { + Ok(Self::Pixels(pixels)) + } else if let Ok(rems) = value.try_into() { + Ok(Self::Rems(rems)) + } else { + Err(anyhow!( + "invalid AbsoluteLength '{value}', expected {EXPECTED_ABSOLUTE_LENGTH}" + )) + } + } +} + +impl JsonSchema for AbsoluteLength { + fn schema_name() -> Cow<'static, str> { + "AbsoluteLength".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem)$" + }) + } +} + +impl<'de> Deserialize<'de> for AbsoluteLength { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = AbsoluteLength; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_ABSOLUTE_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + AbsoluteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for AbsoluteLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +/// A non-auto length that can be defined in pixels, rems, or percent of parent. +/// +/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically +/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a +/// fraction of the parent's size. +#[derive(Clone, Copy, Neg, PartialEq)] +pub enum DefiniteLength { + /// An absolute length specified in pixels or rems. + Absolute(AbsoluteLength), + /// A relative length specified as a fraction of the parent's size, between 0 and 1. + Fraction(f32), +} + +impl DefiniteLength { + /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. + /// + /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. + /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. + /// + /// # Arguments + /// + /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. + /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. + /// + /// # Returns + /// + /// Returns the `DefiniteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use gpui::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; + /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); + /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); + /// let length_as_fraction = DefiniteLength::Fraction(0.5); + /// let base_size = AbsoluteLength::Pixels(px(100.0)); + /// let rem_size = px(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels::from(42.0)); + /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels::from(32.0)); + /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels::from(50.0)); + /// ``` + pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { + match self { + DefiniteLength::Absolute(size) => size.to_pixels(rem_size), + DefiniteLength::Fraction(fraction) => match base_size { + AbsoluteLength::Pixels(px) => px * fraction, + AbsoluteLength::Rems(rems) => rems * rem_size * fraction, + }, + } + } +} + +impl Debug for DefiniteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for DefiniteLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DefiniteLength::Absolute(length) => write!(f, "{length}"), + DefiniteLength::Fraction(fraction) => write!(f, "{}%", (fraction * 100.0) as i32), + } + } +} + +const EXPECTED_DEFINITE_LENGTH: &str = "expected number with 'px', 'rem', or '%' suffix"; + +impl TryFrom<&'_ str> for DefiniteLength { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if let Some(percentage) = value.strip_suffix('%') { + let fraction: f32 = percentage.parse::().with_context(|| { + format!("invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}") + })?; + Ok(DefiniteLength::Fraction(fraction / 100.0)) + } else if let Ok(absolute_length) = value.try_into() { + Ok(DefiniteLength::Absolute(absolute_length)) + } else { + Err(anyhow!( + "invalid DefiniteLength '{value}', expected {EXPECTED_DEFINITE_LENGTH}" + )) + } + } +} + +impl JsonSchema for DefiniteLength { + fn schema_name() -> Cow<'static, str> { + "DefiniteLength".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$" + }) + } +} + +impl<'de> Deserialize<'de> for DefiniteLength { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = DefiniteLength; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_DEFINITE_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + DefiniteLength::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for DefiniteLength { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +impl From for DefiniteLength { + fn from(pixels: Pixels) -> Self { + Self::Absolute(pixels.into()) + } +} + +impl From for DefiniteLength { + fn from(rems: Rems) -> Self { + Self::Absolute(rems.into()) + } +} + +impl From for DefiniteLength { + fn from(length: AbsoluteLength) -> Self { + Self::Absolute(length) + } +} + +impl Default for DefiniteLength { + fn default() -> Self { + Self::Absolute(AbsoluteLength::default()) + } +} + +/// A length that can be defined in pixels, rems, percent of parent, or auto. +#[derive(Clone, Copy, PartialEq)] +pub enum Length { + /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. + Definite(DefiniteLength), + /// An automatic length that is determined by the context in which it is used. + Auto, +} + +impl Debug for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for Length { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Length::Definite(definite_length) => write!(f, "{}", definite_length), + Length::Auto => write!(f, "auto"), + } + } +} + +const EXPECTED_LENGTH: &str = "expected 'auto' or number with 'px', 'rem', or '%' suffix"; + +impl TryFrom<&'_ str> for Length { + type Error = anyhow::Error; + + fn try_from(value: &'_ str) -> Result { + if value == "auto" { + Ok(Length::Auto) + } else if let Ok(definite_length) = value.try_into() { + Ok(Length::Definite(definite_length)) + } else { + Err(anyhow!( + "invalid Length '{value}', expected {EXPECTED_LENGTH}" + )) + } + } +} + +impl JsonSchema for Length { + fn schema_name() -> Cow<'static, str> { + "Length".into() + } + + fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "string", + "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$" + }) + } +} + +impl<'de> Deserialize<'de> for Length { + fn deserialize>(deserializer: D) -> Result { + struct StringVisitor; + + impl de::Visitor<'_> for StringVisitor { + type Value = Length; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{EXPECTED_LENGTH}") + } + + fn visit_str(self, value: &str) -> Result { + Length::try_from(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(StringVisitor) + } +} + +impl Serialize for Length { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{self}")) + } +} + +/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. +/// +/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. +/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. +/// +/// # Arguments +/// +/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. +/// +/// # Returns +/// +/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. +pub const fn relative(fraction: f32) -> DefiniteLength { + DefiniteLength::Fraction(fraction) +} + +/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`. +pub const fn phi() -> DefiniteLength { + relative(1.618_034) +} + +/// Constructs a `Rems` value representing a length in rems. +/// +/// # Arguments +/// +/// * `rems` - The number of rems for the length. +/// +/// # Returns +/// +/// A `Rems` representing the specified number of rems. +pub const fn rems(rems: f32) -> Rems { + Rems(rems) +} + +/// Constructs a `Pixels` value representing a length in pixels. +/// +/// # Arguments +/// +/// * `pixels` - The number of pixels for the length. +/// +/// # Returns +/// +/// A `Pixels` representing the specified number of pixels. +pub const fn px(pixels: f32) -> Pixels { + Pixels(pixels) +} + +/// Returns a `Length` representing an automatic length. +/// +/// The `auto` length is often used in layout calculations where the length should be determined +/// by the layout context itself rather than being explicitly set. This is commonly used in CSS +/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used +/// to instruct the layout engine to calculate the size based on other factors like the size of the +/// container or the intrinsic size of the content. +/// +/// # Returns +/// +/// A `Length` variant set to `Auto`. +pub const fn auto() -> Length { + Length::Auto +} + +impl From for Length { + fn from(pixels: Pixels) -> Self { + Self::Definite(pixels.into()) + } +} + +impl From for Length { + fn from(rems: Rems) -> Self { + Self::Definite(rems.into()) + } +} + +impl From for Length { + fn from(length: DefiniteLength) -> Self { + Self::Definite(length) + } +} + +impl From for Length { + fn from(length: AbsoluteLength) -> Self { + Self::Definite(length.into()) + } +} + +impl Default for Length { + fn default() -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +impl From<()> for Length { + fn from(_: ()) -> Self { + Self::Definite(DefiniteLength::default()) + } +} + +/// A location in a grid layout. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub struct GridLocation { + /// The rows this item uses within the grid. + pub row: Range, + /// The columns this item uses within the grid. + pub column: Range, +} + +/// The placement of an item within a grid layout's column or row. +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] +pub enum GridPlacement { + /// The grid line index to place this item. + Line(i16), + /// The number of grid lines to span. + Span(u16), + /// Automatically determine the placement, equivalent to Span(1) + #[default] + Auto, +} + +impl From for taffy::GridPlacement { + fn from(placement: GridPlacement) -> Self { + match placement { + GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), + GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), + GridPlacement::Auto => taffy::GridPlacement::Auto, + } + } +} + +/// Provides a trait for types that can calculate half of their value. +/// +/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type +/// representing half of the original value. This is commonly used for types that represent measurements or sizes, +/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. +pub trait Half { + /// Returns half of the current value. + /// + /// # Returns + /// + /// A new instance of the implementing type, representing half of the original value. + fn half(&self) -> Self; +} + +impl Half for i32 { + fn half(&self) -> Self { + self / 2 + } +} + +impl Half for f32 { + fn half(&self) -> Self { + self / 2. + } +} + +impl Half for DevicePixels { + fn half(&self) -> Self { + Self(self.0 / 2) + } +} + +impl Half for ScaledPixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Pixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Rems { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +/// A trait for checking if a value is zero. +/// +/// This trait provides a method to determine if a value is considered to be zero. +/// It is implemented for various numeric and length-related types where the concept +/// of zero is applicable. This can be useful for comparisons, optimizations, or +/// determining if an operation has a neutral effect. +pub trait IsZero { + /// Determines if the value is zero. + /// + /// # Returns + /// + /// Returns `true` if the value is zero, `false` otherwise. + fn is_zero(&self) -> bool; +} + +impl IsZero for DevicePixels { + fn is_zero(&self) -> bool { + self.0 == 0 + } +} + +impl IsZero for ScaledPixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Pixels { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for Rems { + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl IsZero for AbsoluteLength { + fn is_zero(&self) -> bool { + match self { + AbsoluteLength::Pixels(pixels) => pixels.is_zero(), + AbsoluteLength::Rems(rems) => rems.is_zero(), + } + } +} + +impl IsZero for DefiniteLength { + fn is_zero(&self) -> bool { + match self { + DefiniteLength::Absolute(length) => length.is_zero(), + DefiniteLength::Fraction(fraction) => *fraction == 0., + } + } +} + +impl IsZero for Length { + fn is_zero(&self) -> bool { + match self { + Length::Definite(length) => length.is_zero(), + Length::Auto => false, + } + } +} + +impl IsZero for Point { + fn is_zero(&self) -> bool { + self.x.is_zero() && self.y.is_zero() + } +} + +impl IsZero for Size +where + T: IsZero + Clone + Debug + Default + PartialEq, +{ + fn is_zero(&self) -> bool { + self.width.is_zero() || self.height.is_zero() + } +} + +impl IsZero for Bounds { + fn is_zero(&self) -> bool { + self.size.is_zero() + } +} + +impl IsZero for Corners +where + T: IsZero + Clone + Debug + Default + PartialEq, +{ + fn is_zero(&self) -> bool { + self.top_left.is_zero() + && self.top_right.is_zero() + && self.bottom_right.is_zero() + && self.bottom_left.is_zero() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bounds_intersects() { + let bounds1 = Bounds { + origin: Point { x: 0.0, y: 0.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds2 = Bounds { + origin: Point { x: 4.0, y: 4.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + let bounds3 = Bounds { + origin: Point { x: 10.0, y: 10.0 }, + size: Size { + width: 5.0, + height: 5.0, + }, + }; + + // Test Case 1: Intersecting bounds + assert!(bounds1.intersects(&bounds2)); + + // Test Case 2: Non-Intersecting bounds + assert!(!bounds1.intersects(&bounds3)); + + // Test Case 3: Bounds intersecting with themselves + assert!(bounds1.intersects(&bounds1)); + } +} diff --git a/vendor/gpui-pre/src/gestures.rs b/vendor/gpui-pre/src/gestures.rs new file mode 100644 index 0000000..ec61157 --- /dev/null +++ b/vendor/gpui-pre/src/gestures.rs @@ -0,0 +1,2211 @@ +//! Touch gesture recognition vocabulary. +//! +//! GPUI recognizes gestures from raw [`TouchEvent`](crate::TouchEvent)s in a +//! single, portable arena in gpui core: recognizers compete for in-flight +//! touches, winners claim them, and losers are cancelled. Recognized gestures +//! are surfaced through *existing* semantic events wherever possible, a tap +//! becomes [`ClickEvent::Touch`](crate::ClickEvent), a pan becomes +//! [`ScrollWheelEvent`](crate::ScrollWheelEvent)s carrying a +//! [`TouchPhase`](crate::TouchPhase), and a pinch becomes +//! [`PinchEvent`](crate::PinchEvent)s — so components written against +//! `on_click` and scroll containers work untouched on mobile. + +use std::collections::VecDeque; +use std::mem; +use std::time::Duration; + +use scheduler::Instant; +use smallvec::SmallVec; + +use crate::{ + Axis, GestureEvent, InputEvent, IsZero, Modifiers, MouseButton, MouseDownEvent, MouseEvent, + MouseUpEvent, Pixels, PlatformInput, Point, ScrollDelta, ScrollWheelEvent, TouchEvent, TouchId, + TouchPhase, point, px, seal::Sealed, +}; + +const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); + +fn dominant_axis(delta: Point) -> Axis { + if delta.x.abs() <= delta.y.abs() { + Axis::Vertical + } else { + Axis::Horizontal + } +} + +fn lock_delta_to_axis(delta: &mut Point, axis: Axis) { + match axis { + Axis::Vertical => delta.x = Pixels::ZERO, + Axis::Horizontal => delta.y = Pixels::ZERO, + } +} + +fn movements_oppose(left: Point, right: Point) -> bool { + f32::from(left.x) * f32::from(right.x) + f32::from(left.y) * f32::from(right.y) < 0. +} + +/// Tracks the dominant axis across the events in a scroll gesture. +#[derive(Clone, Copy, Debug, Default)] +pub struct OngoingScroll { + last_event: Option, + axis: Option, +} + +impl OngoingScroll { + /// Filters the given delta to the dominant axis of the current scroll gesture. + /// + /// Gestures are delimited by their touch phase when available, with a timeout + /// fallback for platforms that only emit [`TouchPhase::Moved`]. + pub fn filter(&mut self, delta: &mut Point, touch_phase: TouchPhase) { + self.filter_at(delta, touch_phase, Instant::now()) + } + + fn filter_at(&mut self, delta: &mut Point, touch_phase: TouchPhase, now: Instant) { + const UNLOCK_PERCENT: f32 = 1.9; + const UNLOCK_LOWER_BOUND: Pixels = px(6.); + + if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) { + self.last_event = None; + self.axis = None; + return; + } + + let x = delta.x.abs(); + let y = delta.y.abs(); + if x.is_zero() && y.is_zero() { + if touch_phase == TouchPhase::Started { + self.last_event = None; + self.axis = None; + } + return; + } + + let starts_new_gesture = touch_phase == TouchPhase::Started + || self + .last_event + .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION); + let mut axis = self.axis; + if starts_new_gesture { + axis = Some(dominant_axis(*delta)); + } else if x.max(y) >= UNLOCK_LOWER_BOUND { + match axis { + Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => { + axis = None; + } + Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => { + axis = None; + } + _ => {} + } + } + + self.last_event = Some(now); + self.axis = axis; + if let Some(axis) = axis { + lock_delta_to_axis(delta, axis); + } + } +} + +/// Feel constants consumed by gesture recognizers. Provided on a best-effort +/// basis, depending on each platform's support, defaulting to GPUI's own +/// (iOS flavored) values +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GestureTuning { + /// Distance a touch may travel before it stops being a potential tap and + /// becomes a pan/drag. + pub touch_slop: Pixels, + /// Maximum interval between taps for them to accumulate a tap count. + pub multi_tap_interval: Duration, + /// Maximum distance between taps for them to accumulate a tap count. + pub multi_tap_slop: Pixels, + /// How long a touch must remain within [`Self::touch_slop`] to be + /// recognized as a long press. + pub long_press_duration: Duration, + /// How scroll momentum decelerates after a fling. + pub scroll_physics: ScrollPhysics, + /// Minimum release velocity, in pixels per second, required to start + /// scroll momentum. + pub min_fling_velocity: f32, +} + +impl Default for GestureTuning { + fn default() -> Self { + Self { + touch_slop: px(8.), + multi_tap_interval: Duration::from_millis(400), + multi_tap_slop: px(16.), + long_press_duration: Duration::from_millis(500), + scroll_physics: ScrollPhysics::ios(), + min_fling_velocity: 50., + } + } +} + +/// How free scrolling decelerates after a fling. +/// +/// This models deceleration only. Boundary behavior — bouncing, edge glow, +/// clamping — is the scroll container's policy: the container is the one that +/// knows its extents. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ScrollPhysics { + /// Exponential velocity decay, the `UIScrollView` model: + /// `velocity(t) = v₀ · decay_per_msᵐˢ`. + Exponential { + /// Per-millisecond velocity decay factor. `UIScrollView`'s normal + /// deceleration rate is `0.998`. + decay_per_ms: f32, + }, + /// The friction spline of Android's `OverScroller`: fling duration and + /// distance follow a logarithmic deceleration law, and progress along + /// the fling follows a cubic-Bezier ease-out curve. Transcribed from + /// AOSP's `SplineOverScroller` (Apache-2.0). + FrictionSpline { + /// The scroll friction coefficient; + /// `ViewConfiguration.getScrollFriction()` is `0.015` on Android. + friction: f32, + /// Pixels per physical inch of the display, in the coordinate space + /// the fling runs in. Android folds display density into its + /// deceleration coefficient, so the same finger speed flings + /// further in pixels on a denser screen. + pixels_per_inch: f32, + }, +} + +impl ScrollPhysics { + /// iOS scroll feel: `UIScrollView`'s normal deceleration rate. + pub fn ios() -> Self { + Self::Exponential { + decay_per_ms: 0.998, + } + } + + /// Android scroll feel: `OverScroller` with stock friction, at Android's + /// nominal density of 160 density-independent pixels per inch — the + /// right pairing when fling distances are in logical pixels. Platforms + /// that fling in physical pixels, or know the display's true density in + /// their logical space, should construct + /// [`ScrollPhysics::FrictionSpline`] directly. + pub fn android() -> Self { + Self::FrictionSpline { + friction: 0.015, + pixels_per_inch: 160., + } + } + + /// How long a fling released at `speed` pixels per second coasts before + /// it stops. + fn fling_duration(self, speed: f32) -> Duration { + match self { + Self::Exponential { decay_per_ms } => { + if speed <= MOMENTUM_STOP_VELOCITY { + return Duration::ZERO; + } + let milliseconds = (MOMENTUM_STOP_VELOCITY / speed).ln() / decay_per_ms.ln(); + Duration::from_secs_f32(milliseconds / 1000.) + } + Self::FrictionSpline { + friction, + pixels_per_inch, + } => { + if speed <= 0. { + return Duration::ZERO; + } + let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch); + let seconds = (deceleration / (friction_spline::deceleration_rate() - 1.)).exp(); + Duration::from_secs_f64(seconds) + } + } + } + + /// Distance traveled `elapsed` into a fling released at `speed` pixels + /// per second, in pixels along the fling direction. Evaluated in closed + /// form so the trajectory is independent of tick timing. + fn fling_distance(self, speed: f32, elapsed: Duration) -> f32 { + let duration = self.fling_duration(speed); + if duration.is_zero() { + return 0.; + } + let elapsed = elapsed.min(duration); + match self { + Self::Exponential { decay_per_ms } => { + // ∫₀ᵗ v₀·kᵐˢ dms, with speed converted to pixels per + // millisecond. + let milliseconds = elapsed.as_secs_f32() * 1000.; + (speed / 1000.) * (decay_per_ms.powf(milliseconds) - 1.) / decay_per_ms.ln() + } + Self::FrictionSpline { + friction, + pixels_per_inch, + } => { + let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch); + let rate = friction_spline::deceleration_rate(); + let total_distance = friction as f64 + * friction_spline::physical_coefficient(pixels_per_inch) + * (rate / (rate - 1.) * deceleration).exp(); + let progress = elapsed.as_secs_f64() / duration.as_secs_f64(); + total_distance as f32 * friction_spline::distance_coefficient(progress as f32) + } + } + } +} + +/// The fling model of Android's `OverScroller.SplineOverScroller`, +/// transcribed from AOSP (Apache-2.0). `SPLINE_TIME`, which AOSP uses for +/// programmatic scroll animations rather than flings, is intentionally not +/// transcribed. +mod friction_spline { + use std::sync::LazyLock; + + const NB_SAMPLES: usize = 100; + const INFLEXION: f32 = 0.35; + const START_TENSION: f32 = 0.5; + const END_TENSION: f32 = 1.0; + const P1: f32 = START_TENSION * INFLEXION; + const P2: f32 = 1.0 - END_TENSION * (1.0 - INFLEXION); + + /// Android's `DECELERATION_RATE`: `ln(0.78) / ln(0.9)`. + pub(super) fn deceleration_rate() -> f64 { + 0.78f64.ln() / 0.9f64.ln() + } + + /// `SPLINE_POSITION` from AOSP's static initializer: fractional fling + /// distance sampled at 100 evenly spaced fractions of the fling + /// duration, from a cubic Bezier with control points shaped by + /// `INFLEXION` and the start/end tensions. + static SPLINE_POSITION: LazyLock<[f32; NB_SAMPLES + 1]> = LazyLock::new(|| { + let mut spline_position = [0f32; NB_SAMPLES + 1]; + let mut x_min = 0f32; + for (i, sample) in spline_position.iter_mut().take(NB_SAMPLES).enumerate() { + let alpha = i as f32 / NB_SAMPLES as f32; + let mut x_max = 1f32; + let (x, coefficient) = loop { + let x = x_min + (x_max - x_min) / 2.; + let coefficient = 3. * x * (1. - x); + let time = coefficient * ((1. - x) * P1 + x * P2) + x * x * x; + if (time - alpha).abs() < 1e-5 { + break (x, coefficient); + } + if time > alpha { + x_max = x; + } else { + x_min = x; + } + }; + *sample = coefficient * ((1. - x) * START_TENSION + x) + x * x * x; + } + spline_position[NB_SAMPLES] = 1.; + spline_position + }); + + /// `SensorManager.GRAVITY_EARTH · 39.37 in/m · ppi · 0.84`, AOSP's + /// `mPhysicalCoeff`: gravity expressed in pixels, times an empirical + /// "look and feel" tuning factor. + pub(super) fn physical_coefficient(pixels_per_inch: f32) -> f64 { + 9.80665 * 39.37 * pixels_per_inch as f64 * 0.84 + } + + /// AOSP's `getSplineDeceleration`. + pub(super) fn deceleration(speed: f32, friction: f32, pixels_per_inch: f32) -> f64 { + (INFLEXION as f64 * speed as f64 + / (friction as f64 * physical_coefficient(pixels_per_inch))) + .ln() + } + + /// Fraction of the total fling distance covered at fraction `time` of + /// the fling duration: table lookup plus linear interpolation, as in + /// `SplineOverScroller.update`. + pub(super) fn distance_coefficient(time: f32) -> f32 { + if time >= 1. { + return 1.; + } + let index = ((NB_SAMPLES as f32 * time) as usize).min(NB_SAMPLES - 1); + let time_lower = index as f32 / NB_SAMPLES as f32; + let time_upper = (index + 1) as f32 / NB_SAMPLES as f32; + let distance_lower = SPLINE_POSITION[index]; + let distance_upper = SPLINE_POSITION[index + 1]; + let velocity_coefficient = (distance_upper - distance_lower) / (time_upper - time_lower); + distance_lower + (time - time_lower) * velocity_coefficient + } + + #[cfg(test)] + pub(super) fn bezier_time_and_position(parameter: f32) -> (f32, f32) { + let coefficient = 3. * parameter * (1. - parameter); + let cubed = parameter * parameter * parameter; + ( + coefficient * ((1. - parameter) * P1 + parameter * P2) + cubed, + coefficient * ((1. - parameter) * START_TENSION + parameter) + cubed, + ) + } + + #[cfg(test)] + pub(super) fn spline_position_samples() -> &'static [f32; NB_SAMPLES + 1] { + &SPLINE_POSITION + } +} + +/// The set of gesture kinds that participate in recognition. +/// +/// Used by [`PlatformGestures::native_recognizers`] to declare which gestures +/// the platform recognizes natively rather than leaving to gpui core's +/// portable recognizers. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct GestureKinds { + /// Tap (and multi-tap), surfaced as [`ClickEvent::Touch`](crate::ClickEvent). + pub tap: bool, + /// Long press, surfaced as [`LongPressEvent`]. + pub long_press: bool, + /// Pan/scroll (including fling momentum), surfaced as + /// [`ScrollWheelEvent`](crate::ScrollWheelEvent)s. + pub pan: bool, + /// Pinch to zoom, surfaced as [`PinchEvent`](crate::PinchEvent)s. + pub pinch: bool, +} + +impl GestureKinds { + /// No gestures; gpui core's portable recognizers handle everything. + pub const NONE: Self = Self { + tap: false, + long_press: false, + pan: false, + pinch: false, + }; + + /// All gesture kinds. + pub const ALL: Self = Self { + tap: true, + long_press: true, + pan: true, + pinch: true, + }; +} + +/// A direct touch drag claimed by an element before touch input becomes a tap, +/// long press, or scrolling gesture. +#[derive(Clone, Debug)] +pub struct TouchDragEvent { + /// The phase of the touch drag. + pub phase: TouchPhase, + /// The position where the touch started. + pub start_position: Point, + /// The touch's current position. + pub position: Point, +} + +impl Sealed for TouchDragEvent {} +impl InputEvent for TouchDragEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::TouchDrag(self) + } +} +impl GestureEvent for TouchDragEvent {} +impl MouseEvent for TouchDragEvent {} + +/// A phased long-press gesture recognized from a touch. +#[derive(Clone, Debug)] +pub struct LongPressEvent { + /// The phase of the long press. + pub phase: TouchPhase, + /// The position where the touch started. + pub start_position: Point, + /// The touch's current position. + pub position: Point, +} + +impl Default for LongPressEvent { + fn default() -> Self { + Self { + phase: TouchPhase::Started, + start_position: Point::default(), + position: Point::default(), + } + } +} + +impl Sealed for LongPressEvent {} +impl InputEvent for LongPressEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::LongPress(self) + } +} +impl GestureEvent for LongPressEvent {} +impl MouseEvent for LongPressEvent {} + +/// Platform gesture recognition services. +/// +/// If your mobile platform supports native gesture recognition, use this +/// to share it with GPUI. +pub trait PlatformGestures { + /// Feel constants for the portable recognizers on this platform. + fn tuning(&self) -> GestureTuning { + GestureTuning::default() + } + + /// The gesture kinds this platform recognizes natively. + fn native_recognizers(&self) -> GestureKinds { + GestureKinds::NONE + } +} + +/// A no-op [`PlatformGestures`] implementation: no native recognizers and +/// default tuning. Suitable for desktop platforms and tests. +pub struct NullPlatformGestures; + +impl PlatformGestures for NullPlatformGestures {} + +/// Ceiling on recognized fling velocity, in pixels per second (matches +/// Flutter's `kMaxFlingVelocity`). +const MAX_FLING_VELOCITY: f32 = 8000.; + +/// Momentum below this speed, in pixels per second, is imperceptible. The +/// exponential model, which never mathematically stops, treats reaching this +/// speed as the end of the fling. (The friction spline has a finite duration +/// of its own.) +const MOMENTUM_STOP_VELOCITY: f32 = 10.; + +/// How far back the release-velocity estimate looks. Samples older than this +/// reflect an earlier part of the gesture, not the speed at release. +const VELOCITY_WINDOW: Duration = Duration::from_millis(100); + +/// A pause between samples longer than this means the finger stopped: +/// anything before the pause describes an earlier motion, not the release +/// (Flutter's `kAssumePointerMoveStoppedMilliseconds`). Touch hardware +/// reports movement every 8–16ms while the finger is in motion. +const VELOCITY_ASSUME_STOPPED_GAP: Duration = Duration::from_millis(40); + +const VELOCITY_MAX_SAMPLES: usize = 20; + +/// The portable recognizer behind raw touch input: it watches the +/// [`TouchEvent`] stream for one touch at a time and resolves it into either +/// a tap or a pan, following the competition model described in the module +/// docs. Pans continue into post-release momentum when the touch lifts at +/// speed; the window drives that phase through [`Self::tick_momentum`]. +/// +/// Taps are currently surfaced as synthesized mouse presses rather than +/// [`ClickEvent::Touch`](crate::ClickEvent), which keeps every existing +/// mouse-driven behavior (click listeners, caret placement, double-tap +/// selection) working before elements grow a direct tap-delivery path. +/// Pinch recognition is not implemented yet, and additional touches are ignored +/// while one is being recognized. +pub(crate) struct TouchGestureRecognizer { + tuning: GestureTuning, + state: TouchGestureState, + momentum: Option, + last_tap: Option, +} + +/// A semantic event recognized from raw touches, ready to dispatch through +/// the window's existing input paths. +#[derive(Debug)] +pub(crate) enum RecognizedTouchGesture { + /// One step of a pan (or of its post-release momentum), delivered to + /// scroll listeners at the pan's starting position. + Scroll(ScrollWheelEvent), + /// A recognized tap, delivered as a synthesized mouse press and release. + Tap { + down: MouseDownEvent, + up: MouseUpEvent, + }, + TouchDrag(TouchDragEvent), + LongPress(LongPressEvent), +} + +enum TouchGestureState { + Idle, + /// The touch is still within `touch_slop` of where it started: it can + /// still resolve into either a tap or a pan. + Pending { + touch: ActiveTouch, + deadline: Instant, + long_press_offered: bool, + touch_drag_offered: bool, + }, + /// The touch exceeded `touch_slop`: it is a pan until it ends, and its + /// movement flows out as scroll events. + Panning { + touch: ActiveTouch, + axis: Axis, + }, + LongPressing(ActiveTouch), + TouchDragging(ActiveTouch), +} + +struct ActiveTouch { + id: TouchId, + start_position: Point, + /// The latest raw position reported for this touch. + last_position: Point, + /// The position pan output has scrolled to so far. While panning this + /// may run ahead of the raw touch by the event's predicted position; + /// the release event targets the raw position again, so the total + /// scrolled distance always converges to the finger's actual travel. + emitted_position: Point, + /// Retained across stationary samples so prediction corrections cannot + /// reverse a pan when integer browser coordinates repeat. + last_movement: Point, + velocity_tracker: VelocityTracker, +} + +struct CompletedTap { + position: Point, + time: Instant, + count: usize, +} + +/// One fling in progress. The trajectory is a closed-form curve of elapsed +/// time — each tick evaluates it and emits the increment — so the fling is +/// exactly frame-rate independent: a stalled frame simply resumes further +/// along the same curve. +struct Momentum { + /// Where the pan started; synthesized scroll events keep hit-testing + /// there so momentum stays with the container the gesture began on. + position: Point, + /// Unit vector of the release velocity. + direction: Point, + axis: Axis, + /// Release speed in pixels per second. + speed: f32, + started_at: Instant, + duration: Duration, + /// Distance already emitted along `direction`, in pixels. + emitted_distance: f32, +} + +impl TouchGestureRecognizer { + pub(crate) fn new(tuning: GestureTuning) -> Self { + Self { + tuning, + state: TouchGestureState::Idle, + momentum: None, + last_tap: None, + } + } + + pub(crate) fn handle_event( + &mut self, + event: &TouchEvent, + ) -> SmallVec<[RecognizedTouchGesture; 2]> { + self.handle_event_at(event, Instant::now()) + } + + fn handle_event_at( + &mut self, + event: &TouchEvent, + now: Instant, + ) -> SmallVec<[RecognizedTouchGesture; 2]> { + let mut recognized = SmallVec::new(); + match event.phase { + TouchPhase::Started => { + let caught_fling = if let Some(momentum) = self.momentum.take() { + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + momentum.position, + Point::default(), + TouchPhase::Ended, + ))); + Some(momentum.axis) + } else { + None + }; + if matches!(self.state, TouchGestureState::Idle) { + let mut velocity_tracker = VelocityTracker::default(); + velocity_tracker.push(now, event.position); + let touch = ActiveTouch { + id: event.id, + start_position: event.position, + last_position: event.position, + emitted_position: event.position, + last_movement: Point::default(), + velocity_tracker, + }; + if let Some(axis) = caught_fling { + // A touch that catches a fling is a drag from the + // first pixel: waiting out the slop would freeze the + // content mid-scroll and then jump. It can also never + // be a tap; releasing it just leaves the content + // stopped, as on Android and iOS. + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + touch.start_position, + Point::default(), + TouchPhase::Started, + ))); + self.state = TouchGestureState::Panning { touch, axis }; + } else { + self.state = TouchGestureState::Pending { + touch, + deadline: now + self.tuning.long_press_duration, + long_press_offered: false, + touch_drag_offered: false, + }; + } + } + } + TouchPhase::Moved => match mem::replace(&mut self.state, TouchGestureState::Idle) { + TouchGestureState::Pending { + mut touch, + deadline, + long_press_offered, + touch_drag_offered, + } if touch.id == event.id => { + touch.velocity_tracker.push(now, event.position); + touch.last_position = event.position; + let accumulated = event.position - touch.start_position; + if accumulated.magnitude() > f64::from(self.tuning.touch_slop) { + // Carry the full movement so far into the first scroll + // step: the content catches up to the finger instead + // of losing the slop distance. + let mut target = event.predicted_position.unwrap_or(event.position); + let axis = dominant_axis(accumulated); + let mut delta = target - touch.start_position; + lock_delta_to_axis(&mut delta, axis); + touch.last_movement = accumulated; + lock_delta_to_axis(&mut touch.last_movement, axis); + if movements_oppose(delta, touch.last_movement) { + target = event.position; + delta = accumulated; + lock_delta_to_axis(&mut delta, axis); + } + touch.emitted_position = target; + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + touch.start_position, + delta, + TouchPhase::Started, + ))); + self.state = TouchGestureState::Panning { touch, axis }; + } else { + self.state = TouchGestureState::Pending { + touch, + deadline, + long_press_offered, + touch_drag_offered, + }; + } + } + TouchGestureState::Panning { mut touch, axis } if touch.id == event.id => { + let mut raw_delta = event.position - touch.last_position; + lock_delta_to_axis(&mut raw_delta, axis); + if raw_delta != Point::default() { + touch.last_movement = raw_delta; + } + touch.velocity_tracker.push(now, event.position); + touch.last_position = event.position; + let mut target = event.predicted_position.unwrap_or(event.position); + let mut delta = target - touch.emitted_position; + lock_delta_to_axis(&mut delta, axis); + // Prediction error must not reverse content while the raw + // touch still advances. Fall back to the raw position so a + // real finger reversal remains responsive. + if movements_oppose(delta, touch.last_movement) { + target = event.position; + delta = target - touch.emitted_position; + lock_delta_to_axis(&mut delta, axis); + if movements_oppose(delta, touch.last_movement) { + target = touch.emitted_position; + delta = Point::default(); + } + } + touch.emitted_position = target; + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + touch.start_position, + delta, + TouchPhase::Moved, + ))); + self.state = TouchGestureState::Panning { touch, axis }; + } + TouchGestureState::LongPressing(mut touch) if touch.id == event.id => { + touch.last_position = event.position; + recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { + phase: TouchPhase::Moved, + start_position: touch.start_position, + position: event.position, + })); + self.state = TouchGestureState::LongPressing(touch); + } + TouchGestureState::TouchDragging(mut touch) if touch.id == event.id => { + touch.last_position = event.position; + recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { + phase: TouchPhase::Moved, + start_position: touch.start_position, + position: event.position, + })); + self.state = TouchGestureState::TouchDragging(touch); + } + other => self.state = other, + }, + TouchPhase::Ended => match mem::replace(&mut self.state, TouchGestureState::Idle) { + TouchGestureState::Pending { touch, .. } if touch.id == event.id => { + let tap_count = match &self.last_tap { + Some(tap) + if now.duration_since(tap.time) <= self.tuning.multi_tap_interval + && (event.position - tap.position).magnitude() + <= f64::from(self.tuning.multi_tap_slop) => + { + tap.count + 1 + } + _ => 1, + }; + self.last_tap = Some(CompletedTap { + position: event.position, + time: now, + count: tap_count, + }); + recognized.push(RecognizedTouchGesture::Tap { + down: MouseDownEvent { + button: MouseButton::Left, + position: event.position, + modifiers: Modifiers::default(), + click_count: tap_count, + first_mouse: false, + }, + up: MouseUpEvent { + button: MouseButton::Left, + position: event.position, + modifiers: Modifiers::default(), + click_count: tap_count, + }, + }); + } + TouchGestureState::Panning { touch, axis } if touch.id == event.id => { + // The release deliberately contributes no velocity + // sample: it usually repeats the last movement's position + // with a later timestamp, which would dilute the + // estimate. But a release long after the last movement + // means the finger had already stopped, so nothing + // flings. + let finger_stopped = + touch + .velocity_tracker + .latest_sample_time() + .is_none_or(|latest| { + now.duration_since(latest) > VELOCITY_ASSUME_STOPPED_GAP + }); + let mut velocity = if finger_stopped { + Point::default() + } else { + touch.velocity_tracker.velocity() + }; + match axis { + Axis::Vertical => velocity.x = 0., + Axis::Horizontal => velocity.y = 0., + } + let speed = (velocity.x.powi(2) + velocity.y.powi(2)).sqrt(); + let mut release_delta = event.position - touch.emitted_position; + lock_delta_to_axis(&mut release_delta, axis); + if speed >= self.tuning.min_fling_velocity { + let direction = point(velocity.x / speed, velocity.y / speed); + let speed = speed.min(MAX_FLING_VELOCITY); + let duration = self.tuning.scroll_physics.fling_duration(speed); + if !duration.is_zero() { + let total_distance = + self.tuning.scroll_physics.fling_distance(speed, duration); + // Prediction may have left the content ahead of + // the raw release position. Emitting that + // correction here would visibly snap the content + // backwards just as the fling launches, so fold + // it into the fling instead: start the curve + // already advanced by the overshoot, keeping the + // total travel exact while staying monotonic. + let overshoot = -(f32::from(release_delta.x) * direction.x + + f32::from(release_delta.y) * direction.y); + let emitted_distance = if overshoot > 0. && overshoot < total_distance { + release_delta += + point(px(direction.x * overshoot), px(direction.y * overshoot)); + overshoot + } else { + 0. + }; + self.momentum = Some(Momentum { + position: touch.start_position, + direction, + axis, + speed, + started_at: now, + duration, + emitted_distance, + }); + } + } + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + touch.start_position, + release_delta, + TouchPhase::Ended, + ))); + } + TouchGestureState::LongPressing(touch) if touch.id == event.id => { + recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { + phase: TouchPhase::Ended, + start_position: touch.start_position, + position: event.position, + })); + } + TouchGestureState::TouchDragging(touch) if touch.id == event.id => { + recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { + phase: TouchPhase::Ended, + start_position: touch.start_position, + position: event.position, + })); + } + other => self.state = other, + }, + TouchPhase::Cancelled => match mem::replace(&mut self.state, TouchGestureState::Idle) { + TouchGestureState::Pending { touch, .. } if touch.id == event.id => {} + TouchGestureState::Panning { touch, .. } if touch.id == event.id => { + recognized.push(RecognizedTouchGesture::Scroll(scroll_event( + touch.start_position, + Point::default(), + TouchPhase::Cancelled, + ))); + } + TouchGestureState::LongPressing(touch) if touch.id == event.id => { + recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent { + phase: TouchPhase::Cancelled, + start_position: touch.start_position, + position: event.position, + })); + } + TouchGestureState::TouchDragging(touch) if touch.id == event.id => { + recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent { + phase: TouchPhase::Cancelled, + start_position: touch.start_position, + position: event.position, + })); + } + other => self.state = other, + }, + } + recognized + } + + pub(crate) fn pending_long_press(&self) -> Option<(TouchId, Duration)> { + let TouchGestureState::Pending { + touch, + deadline, + long_press_offered: false, + .. + } = &self.state + else { + return None; + }; + Some((touch.id, deadline.saturating_duration_since(Instant::now()))) + } + + pub(crate) fn offer_long_press(&mut self, id: TouchId) -> Option { + let TouchGestureState::Pending { + touch, + long_press_offered, + .. + } = &mut self.state + else { + return None; + }; + if touch.id != id || *long_press_offered { + return None; + } + *long_press_offered = true; + Some(RecognizedTouchGesture::LongPress(LongPressEvent { + phase: TouchPhase::Started, + start_position: touch.start_position, + position: touch.last_position, + })) + } + + pub(crate) fn resolve_long_press(&mut self, claimed: bool) { + if !claimed { + return; + } + let state = mem::replace(&mut self.state, TouchGestureState::Idle); + self.state = match state { + TouchGestureState::Pending { + touch, + long_press_offered: true, + .. + } => TouchGestureState::LongPressing(touch), + other => other, + }; + } + + pub(crate) fn offer_touch_drag(&mut self, id: TouchId) -> Option { + let TouchGestureState::Pending { + touch, + touch_drag_offered, + .. + } = &mut self.state + else { + return None; + }; + if touch.id != id || *touch_drag_offered { + return None; + } + *touch_drag_offered = true; + Some(RecognizedTouchGesture::TouchDrag(TouchDragEvent { + phase: TouchPhase::Started, + start_position: touch.start_position, + position: touch.last_position, + })) + } + + pub(crate) fn resolve_touch_drag(&mut self, claimed: bool) { + if !claimed { + return; + } + let state = mem::replace(&mut self.state, TouchGestureState::Idle); + self.state = match state { + TouchGestureState::Pending { + touch, + touch_drag_offered: true, + .. + } => TouchGestureState::TouchDragging(touch), + other => other, + }; + } + + pub(crate) fn has_momentum(&self) -> bool { + self.momentum.is_some() + } + + /// Advances post-fling momentum by one frame, returning the scroll step + /// to dispatch, or `None` when no momentum is in progress. The final step + /// carries [`TouchPhase::Ended`] to close the synthetic scroll stream. + pub(crate) fn tick_momentum(&mut self) -> Option { + self.tick_momentum_at(Instant::now()) + } + + fn tick_momentum_at(&mut self, now: Instant) -> Option { + let momentum = self.momentum.as_mut()?; + let elapsed = now.duration_since(momentum.started_at); + let distance = self + .tuning + .scroll_physics + .fling_distance(momentum.speed, elapsed); + // Prediction overshoot can start momentum ahead of its curve. Hold + // that position until the curve catches up instead of stepping back. + let step = (distance - momentum.emitted_distance).max(0.); + momentum.emitted_distance = momentum.emitted_distance.max(distance); + let delta = point( + px(momentum.direction.x * step), + px(momentum.direction.y * step), + ); + let position = momentum.position; + if elapsed >= momentum.duration { + self.momentum = None; + Some(RecognizedTouchGesture::Scroll(scroll_event( + position, + delta, + TouchPhase::Ended, + ))) + } else { + Some(RecognizedTouchGesture::Scroll(scroll_event( + position, + delta, + TouchPhase::Moved, + ))) + } + } +} + +fn scroll_event( + position: Point, + delta: Point, + touch_phase: TouchPhase, +) -> ScrollWheelEvent { + ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(delta), + modifiers: Modifiers::default(), + touch_phase, + } +} + +/// Estimates the velocity a touch had at its newest sample. +#[derive(Default)] +struct VelocityTracker { + samples: VecDeque<(Instant, Point)>, +} + +impl VelocityTracker { + fn push(&mut self, time: Instant, position: Point) { + self.samples.push_back((time, position)); + while self.samples.len() > VELOCITY_MAX_SAMPLES { + self.samples.pop_front(); + } + } + + fn latest_sample_time(&self) -> Option { + self.samples.back().map(|(time, _)| *time) + } + + /// The velocity at the newest sample, in pixels per second. + /// + /// Fits a second-degree polynomial by least squares over the trailing + /// [`VELOCITY_WINDOW`] and takes its derivative at the newest sample, + /// like Flutter's `VelocityTracker` and Android's `lsq2` strategy. An + /// endpoint difference over the same window would report the window's + /// *average* speed, which for a flick — still accelerating at lift-off — + /// is roughly half the speed the finger actually had at release. + fn velocity(&self) -> Point { + let Some((newest_time, _)) = self.samples.back() else { + return Point::default(); + }; + let mut times_seconds: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); + let mut horizontal: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); + let mut vertical: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new(); + let mut previous_time = *newest_time; + for (time, position) in self.samples.iter().rev() { + let age = newest_time.duration_since(*time); + if age > VELOCITY_WINDOW + || previous_time.duration_since(*time) > VELOCITY_ASSUME_STOPPED_GAP + { + break; + } + previous_time = *time; + times_seconds.push(-age.as_secs_f64()); + horizontal.push(f64::from(f32::from(position.x))); + vertical.push(f64::from(f32::from(position.y))); + } + + let endpoint_estimate = |values: &[f64]| -> f32 { + let elapsed = -times_seconds.last().copied().unwrap_or(0.); + if elapsed <= f64::EPSILON { + return 0.; + } + ((values.first().copied().unwrap_or(0.) - values.last().copied().unwrap_or(0.)) + / elapsed) as f32 + }; + if times_seconds.len() < 3 { + return point(endpoint_estimate(&horizontal), endpoint_estimate(&vertical)); + } + point( + quadratic_velocity_at_newest(×_seconds, &horizontal).map_or_else( + || endpoint_estimate(&horizontal), + |velocity| velocity as f32, + ), + quadratic_velocity_at_newest(×_seconds, &vertical) + .map_or_else(|| endpoint_estimate(&vertical), |velocity| velocity as f32), + ) + } +} + +/// Least-squares fit of `value = a0 + a1·t + a2·t²` returning `a1`: the +/// fitted curve's velocity at `t = 0`, which callers place at the newest +/// sample. `None` when the samples are too degenerate to fit (all +/// simultaneous, for example). +fn quadratic_velocity_at_newest(times: &[f64], values: &[f64]) -> Option { + let count = times.len() as f64; + let (mut sum_t1, mut sum_t2, mut sum_t3, mut sum_t4) = (0., 0., 0., 0.); + let (mut sum_v, mut sum_vt, mut sum_vt2) = (0., 0., 0.); + for (&time, &value) in times.iter().zip(values) { + let time_squared = time * time; + sum_t1 += time; + sum_t2 += time_squared; + sum_t3 += time_squared * time; + sum_t4 += time_squared * time_squared; + sum_v += value; + sum_vt += value * time; + sum_vt2 += value * time_squared; + } + // Cramer's rule on the 3×3 normal equations, solved for the linear + // coefficient only. + let determinant = count * (sum_t2 * sum_t4 - sum_t3 * sum_t3) + - sum_t1 * (sum_t1 * sum_t4 - sum_t3 * sum_t2) + + sum_t2 * (sum_t1 * sum_t3 - sum_t2 * sum_t2); + if determinant.abs() < 1e-12 { + return None; + } + let linear_determinant = count * (sum_vt * sum_t4 - sum_t3 * sum_vt2) + - sum_v * (sum_t1 * sum_t4 - sum_t3 * sum_t2) + + sum_t2 * (sum_t1 * sum_vt2 - sum_vt * sum_t2); + Some(linear_determinant / determinant) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::point; + + #[test] + fn ongoing_scroll_locks_to_dominant_axis() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); + assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); + assert_eq!(horizontal_delta, point(px(10.), px(0.))); + + let mut continued_delta = point(px(3.), px(2.)); + ongoing_scroll.filter_at( + &mut continued_delta, + TouchPhase::Moved, + now + Duration::from_millis(1), + ); + assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); + assert_eq!(continued_delta, point(px(3.), px(0.))); + } + + #[test] + fn ongoing_scroll_unlocks_when_direction_changes() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); + + let mut vertical_delta = point(px(2.), px(10.)); + ongoing_scroll.filter_at( + &mut vertical_delta, + TouchPhase::Moved, + now + Duration::from_millis(1), + ); + assert_eq!(ongoing_scroll.axis, None); + assert_eq!(vertical_delta, point(px(2.), px(10.))); + } + + #[test] + fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now); + + let mut vertical_delta = point(px(2.), px(10.)); + ongoing_scroll.filter_at( + &mut vertical_delta, + TouchPhase::Moved, + now + SCROLL_EVENT_SEPARATION, + ); + assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); + assert_eq!(vertical_delta, point(px(0.), px(10.))); + } + + #[test] + fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); + + let mut zero_delta = Point::default(); + ongoing_scroll.filter_at( + &mut zero_delta, + TouchPhase::Ended, + now + Duration::from_millis(1), + ); + assert_eq!(ongoing_scroll.axis, None); + + let mut vertical_delta = point(px(2.), px(3.)); + ongoing_scroll.filter_at( + &mut vertical_delta, + TouchPhase::Moved, + now + Duration::from_millis(2), + ); + assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); + assert_eq!(vertical_delta, point(px(0.), px(3.))); + } + + #[test] + fn ongoing_scroll_ignores_zero_delta_movement() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now); + + let mut zero_delta = Point::default(); + ongoing_scroll.filter_at( + &mut zero_delta, + TouchPhase::Moved, + now + SCROLL_EVENT_SEPARATION, + ); + + let mut vertical_delta = point(px(2.), px(10.)); + ongoing_scroll.filter_at( + &mut vertical_delta, + TouchPhase::Moved, + now + SCROLL_EVENT_SEPARATION, + ); + assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical)); + assert_eq!(vertical_delta, point(px(0.), px(10.))); + } + + #[test] + fn ongoing_scroll_supports_moved_only_platforms() { + let now = Instant::now(); + let mut ongoing_scroll = OngoingScroll::default(); + let mut horizontal_delta = point(px(10.), px(2.)); + ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now); + assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal)); + assert_eq!(horizontal_delta, point(px(10.), px(0.))); + } + + #[test] + fn touch_within_slop_resolves_to_tap() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + let recognized = + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 10.), now); + assert!(recognized.is_empty()); + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 12., 11.), + now + Duration::from_millis(20), + ); + assert!(recognized.is_empty()); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 12., 11.), + now + Duration::from_millis(60), + ); + let [RecognizedTouchGesture::Tap { down, up }] = recognized.as_slice() else { + panic!("expected tap, got {recognized:?}"); + }; + assert_eq!(down.click_count, 1); + assert_eq!(down.position, point(px(12.), px(11.))); + assert_eq!(up.click_count, 1); + } + + #[test] + fn consecutive_taps_accumulate_tap_count() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + + recognizer.handle_event_at(&touch_event(TouchId(1), TouchPhase::Started, 10., 10.), now); + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Ended, 10., 10.), + now + Duration::from_millis(40), + ); + + let second_down = now + Duration::from_millis(200); + recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Started, 14., 10.), + second_down, + ); + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Ended, 14., 10.), + second_down + Duration::from_millis(40), + ); + let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else { + panic!("expected tap, got {recognized:?}"); + }; + assert_eq!(down.click_count, 2); + + let late_down = second_down + Duration::from_secs(2); + recognizer.handle_event_at( + &touch_event(TouchId(3), TouchPhase::Started, 14., 10.), + late_down, + ); + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(3), TouchPhase::Ended, 14., 10.), + late_down + Duration::from_millis(40), + ); + let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else { + panic!("expected tap, got {recognized:?}"); + }; + assert_eq!(down.click_count, 1); + } + + #[test] + fn touch_beyond_slop_resolves_to_pan() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 120.), + now + Duration::from_millis(16), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Started); + assert_eq!(scroll.position, point(px(100.), px(100.))); + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.))); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 135.), + now + Duration::from_millis(32), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Moved); + assert_eq!(scroll.position, point(px(100.), px(100.))); + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(15.))); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 100., 135.), + now + Duration::from_millis(48), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Ended); + } + + #[test] + fn touch_pan_stays_locked_to_its_initial_dominant_axis() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 104., 120.), + now + Duration::from_millis(16), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.))); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 134., 125.), + now + Duration::from_millis(32), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(5.))); + } + + #[test] + fn touch_pan_locks_to_horizontal_axis() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 120., 104.), + now + Duration::from_millis(16), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(20.), px(0.))); + } + + #[test] + fn predicted_positions_lead_the_pan_but_totals_converge_on_release() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + + // The first pan step scrolls to the predicted position, not the raw one. + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.); + moved.predicted_position = Some(point(px(106.), px(128.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(28.))); + + // The next step is measured from where the previous prediction left + // the content, so an overshoot is paid back here. + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 130.); + moved.predicted_position = Some(point(px(104.), px(134.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.))); + + // A release without a fling (the finger stopped long before lifting) + // targets the raw position: the total scrolled distance equals the + // finger's actual travel despite the predictions. + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 100., 130.), + now + Duration::from_millis(120), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Ended); + assert!(!recognizer.has_momentum()); + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-4.))); + } + + #[test] + fn predicted_positions_do_not_emit_false_reversals() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.); + moved.predicted_position = Some(point(px(100.), px(130.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(30.))); + + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.); + moved.predicted_position = Some(point(px(100.), px(127.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!( + scroll.delta.pixel_delta(px(16.)), + Point::::default() + ); + + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.); + moved.predicted_position = Some(point(px(100.), px(126.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(40)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!( + scroll.delta.pixel_delta(px(16.)), + Point::::default() + ); + + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 132.); + moved.predicted_position = Some(point(px(100.), px(136.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(48)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.))); + + let mut moved = touch_event(touch, TouchPhase::Moved, 100., 124.); + moved.predicted_position = Some(point(px(100.), px(140.))); + let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(64)); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-12.))); + } + + #[test] + fn predicted_overshoot_folds_into_the_fling_without_scrolling_backwards() { + let now = Instant::now(); + let mut total_with_prediction = 0f32; + let mut total_without_prediction = 0f32; + for use_prediction in [true, false] { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let mut total = 0f32; + let mut drain = |recognized: &[RecognizedTouchGesture], upward_only: bool| { + for gesture in recognized { + let RecognizedTouchGesture::Scroll(scroll) = gesture else { + panic!("expected scroll, got {gesture:?}"); + }; + let delta = scroll.delta.pixel_delta(px(16.)).y; + if upward_only { + assert!( + delta <= px(0.), + "content moved backwards by {delta:?} during an upward gesture" + ); + } + total += f32::from(delta); + } + }; + + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Started, 100., 500.), + now, + ); + for step in 1..=5u64 { + let raw_y = 500. - step as f32 * 40.; + let mut moved = touch_event(TouchId(1), TouchPhase::Moved, 100., raw_y); + if use_prediction { + moved.predicted_position = Some(point(px(100.), px(raw_y - 25.))); + } + let recognized = + recognizer.handle_event_at(&moved, now + Duration::from_millis(step * 16)); + drain(&recognized, use_prediction); + } + // The release leaves the emitted position 25px ahead of the raw + // one; with prediction the correction must not scroll backwards. + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Ended, 100., 300.), + now + Duration::from_millis(90), + ); + drain(&recognized, use_prediction); + assert!(recognizer.has_momentum()); + let mut tick = now + Duration::from_millis(91); + while recognizer.has_momentum() { + if let Some(gesture) = recognizer.tick_momentum_at(tick) { + drain(&[gesture], use_prediction); + } + tick += Duration::from_millis(16); + } + + if use_prediction { + total_with_prediction = total; + } else { + total_without_prediction = total; + } + } + // Folding the overshoot into the fling redistributes the travel but + // must not change where the content comes to rest. + assert!( + (total_with_prediction - total_without_prediction).abs() < 0.01, + "totals diverged: {total_with_prediction} vs {total_without_prediction}" + ); + } + + #[test] + fn fast_release_starts_momentum_that_decays_to_a_stop() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); + for step in 1..=5 { + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 300. - step as f32 * 20.), + now + Duration::from_millis(step * 16), + ); + } + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 100., 200.), + now + Duration::from_millis(6 * 16), + ); + assert!(recognizer.has_momentum()); + + let tick = now + Duration::from_millis(6 * 16 + 16); + let recognized = recognizer.tick_momentum_at(tick); + let Some(RecognizedTouchGesture::Scroll(scroll)) = recognized else { + panic!("expected momentum scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Moved); + assert_eq!(scroll.position, point(px(100.), px(300.))); + let delta = scroll.delta.pixel_delta(px(16.)); + assert!( + delta.y < px(0.), + "momentum should continue upward, got {delta:?}" + ); + // The least-squares fit may leave float residue on the motionless axis. + assert!( + delta.x.abs() < px(0.001), + "expected no x motion, got {delta:?}" + ); + + let mut last_phase = TouchPhase::Moved; + let mut ticks = 0; + let mut time = tick; + while recognizer.has_momentum() { + time += Duration::from_millis(16); + ticks += 1; + assert!(ticks < 1000, "momentum never stopped"); + if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time) + { + last_phase = scroll.touch_phase; + } + } + assert_eq!(last_phase, TouchPhase::Ended); + assert!(recognizer.tick_momentum_at(time).is_none()); + } + + #[test] + fn diagonal_release_flings_only_on_locked_axis() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); + for step in 1..=5 { + let recognized = recognizer.handle_event_at( + &touch_event( + touch, + TouchPhase::Moved, + 100. + step as f32 * 3., + 300. - step as f32 * 20., + ), + now + Duration::from_millis(step * 16), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.delta.pixel_delta(px(16.)).x, px(0.)); + } + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 115., 200.), + now + Duration::from_millis(6 * 16), + ); + assert!(recognizer.has_momentum()); + + let mut time = now + Duration::from_millis(6 * 16); + while recognizer.has_momentum() { + time += Duration::from_millis(16); + if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time) + { + let delta = scroll.delta.pixel_delta(px(16.)); + assert_eq!(delta.x, px(0.)); + assert!(delta.y <= px(0.)); + } + } + } + + #[test] + fn slow_release_does_not_start_momentum() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now); + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 280.), + now + Duration::from_millis(16), + ); + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 279.), + now + Duration::from_millis(500), + ); + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 100., 279.), + now + Duration::from_millis(600), + ); + assert!(!recognizer.has_momentum()); + } + + #[test] + fn new_touch_interrupts_momentum() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Started, 100., 300.), + now, + ); + for step in 1..=3 { + recognizer.handle_event_at( + &touch_event( + TouchId(1), + TouchPhase::Moved, + 100., + 300. - step as f32 * 33., + ), + now + Duration::from_millis(step * 16), + ); + } + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.), + now + Duration::from_millis(64), + ); + assert!(recognizer.has_momentum()); + + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Started, 100., 200.), + now + Duration::from_millis(200), + ); + assert!(!recognizer.has_momentum()); + let [ + RecognizedTouchGesture::Scroll(closing), + RecognizedTouchGesture::Scroll(opening), + ] = recognized.as_slice() + else { + panic!("expected closing and opening scrolls, got {recognized:?}"); + }; + assert_eq!(closing.touch_phase, TouchPhase::Ended); + assert!(closing.delta.pixel_delta(px(16.)).is_zero()); + assert_eq!(opening.touch_phase, TouchPhase::Started); + assert!(opening.delta.pixel_delta(px(16.)).is_zero()); + } + + #[test] + fn catching_a_fling_pans_immediately_and_never_taps() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Started, 100., 300.), + now, + ); + for step in 1..=3 { + recognizer.handle_event_at( + &touch_event( + TouchId(1), + TouchPhase::Moved, + 100., + 300. - step as f32 * 33., + ), + now + Duration::from_millis(step * 16), + ); + } + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.), + now + Duration::from_millis(64), + ); + assert!(recognizer.has_momentum()); + + recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Started, 100., 200.), + now + Duration::from_millis(200), + ); + + // A movement well within the slop scrolls immediately. + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Moved, 100., 197.), + now + Duration::from_millis(216), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Moved); + assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-3.))); + + // Releasing the catch is not a tap. + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Ended, 100., 197.), + now + Duration::from_millis(232), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Ended); + } + + #[test] + fn cancelled_pan_emits_cancelled_scroll_and_no_tap() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + let touch = TouchId(1); + + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 100., 150.), + now + Duration::from_millis(16), + ); + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Cancelled, 100., 150.), + now + Duration::from_millis(32), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected cancelled scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Cancelled); + assert!(!recognizer.has_momentum()); + + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now); + let recognized = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Cancelled, 100., 102.), + now + Duration::from_millis(16), + ); + assert!(recognized.is_empty(), "cancelled tap must not click"); + } + + #[test] + fn concurrent_touches_are_ignored_while_one_is_active() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let now = Instant::now(); + + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Started, 100., 100.), + now, + ); + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Started, 200., 200.), + now + Duration::from_millis(8), + ); + assert!(recognized.is_empty()); + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Moved, 200., 300.), + now + Duration::from_millis(16), + ); + assert!(recognized.is_empty()); + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(2), TouchPhase::Ended, 200., 300.), + now + Duration::from_millis(24), + ); + assert!(recognized.is_empty()); + + // The first touch still resolves normally. + let recognized = recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Moved, 100., 150.), + now + Duration::from_millis(32), + ); + let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else { + panic!("expected scroll, got {recognized:?}"); + }; + assert_eq!(scroll.touch_phase, TouchPhase::Started); + } + + #[test] + fn spline_position_table_matches_the_bezier_curve() { + let samples = friction_spline::spline_position_samples(); + // AOSP's initializer solves sample 0 numerically like every other + // sample, so it lands within solver tolerance of zero, not at zero. + assert!(samples[0].abs() < 1e-4); + assert_eq!(samples[100], 1.); + for window in samples.windows(2) { + assert!(window[0] < window[1], "table must be strictly increasing"); + } + // Each table entry must lie on the defining parametric Bezier: for + // sample i there must be a curve parameter whose time component is + // i/100 and whose position component is the stored value. + for (i, &stored_position) in samples.iter().enumerate().take(100) { + let alpha = i as f32 / 100.; + let (mut lower, mut upper) = (0f32, 1f32); + for _ in 0..50 { + let middle = (lower + upper) / 2.; + let (time, _) = friction_spline::bezier_time_and_position(middle); + if time > alpha { + upper = middle; + } else { + lower = middle; + } + } + let (time, position) = friction_spline::bezier_time_and_position((lower + upper) / 2.); + assert!( + (time - alpha).abs() < 1e-4, + "sample {i}: time {time} != {alpha}" + ); + assert!( + (position - stored_position).abs() < 1e-3, + "sample {i}: position {position} != stored {stored_position}" + ); + } + } + + #[test] + fn fling_curves_are_sane_for_both_physics() { + for physics in [ScrollPhysics::ios(), ScrollPhysics::android()] { + let slow = physics.fling_duration(500.); + let fast = physics.fling_duration(4000.); + assert!(slow > Duration::ZERO, "{physics:?}"); + assert!(fast > slow, "faster flings must coast longer: {physics:?}"); + + let halfway = physics.fling_distance(4000., fast / 2); + let total = physics.fling_distance(4000., fast); + assert!(halfway > 0. && halfway < total, "{physics:?}"); + assert!( + physics.fling_distance(4000., fast * 2) == total, + "distance must not grow past the fling duration: {physics:?}" + ); + assert!( + physics.fling_distance(4000., fast) > physics.fling_distance(500., slow), + "faster flings must travel further: {physics:?}" + ); + } + } + + #[test] + fn momentum_is_frame_rate_independent() { + // The same fling ticked at 60Hz and as one huge stalled frame must + // cover identical ground. + let total_distance_with_tick_length = |tick: Duration| -> f32 { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning { + scroll_physics: ScrollPhysics::android(), + ..GestureTuning::default() + }); + let now = Instant::now(); + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Started, 100., 500.), + now, + ); + for step in 1..=3 { + recognizer.handle_event_at( + &touch_event( + TouchId(1), + TouchPhase::Moved, + 100., + 500. - step as f32 * 40., + ), + now + Duration::from_millis(step * 16), + ); + } + recognizer.handle_event_at( + &touch_event(TouchId(1), TouchPhase::Ended, 100., 380.), + now + Duration::from_millis(64), + ); + assert!(recognizer.has_momentum()); + + let mut total = 0f32; + let mut time = now + Duration::from_millis(64); + let mut guard = 0; + while recognizer.has_momentum() { + time += tick; + guard += 1; + assert!(guard < 10_000, "momentum never stopped"); + if let Some(RecognizedTouchGesture::Scroll(scroll)) = + recognizer.tick_momentum_at(time) + { + total += f32::from(scroll.delta.pixel_delta(px(16.)).y); + } + } + total + }; + + let smooth = total_distance_with_tick_length(Duration::from_millis(16)); + let stalled = total_distance_with_tick_length(Duration::from_secs(10)); + assert!( + (smooth - stalled).abs() < 0.01, + "expected identical fling distance, got {smooth} vs {stalled}" + ); + } + + #[test] + fn flick_velocity_reflects_release_speed_not_window_average() { + // A uniformly accelerating flick: position grows quadratically, so + // the speed at the newest sample (2·k·t) is twice the window + // average (k·t). The estimator must report the former. + let mut velocity_tracker = VelocityTracker::default(); + let start = Instant::now(); + for step in 0..=6 { + let t = step as f32 * 0.016; + velocity_tracker.push( + start + Duration::from_millis(step * 16), + point(px(0.), px(1000. * t * t)), + ); + } + let velocity = velocity_tracker.velocity(); + let release_speed = 2. * 1000. * 0.096; + assert!( + (velocity.y - release_speed).abs() < 1., + "expected ≈{release_speed} px/s at release, got {} px/s", + velocity.y + ); + assert_eq!(velocity.x, 0.); + } + + #[test] + fn samples_before_a_pause_do_not_contribute_velocity() { + // Fast motion, then a hold longer than the stopped-finger gap, then + // a slow nudge: only the motion after the pause describes the + // release. + let mut velocity_tracker = VelocityTracker::default(); + let start = Instant::now(); + velocity_tracker.push(start, point(px(0.), px(0.))); + velocity_tracker.push(start + Duration::from_millis(16), point(px(0.), px(50.))); + velocity_tracker.push(start + Duration::from_millis(80), point(px(0.), px(52.))); + velocity_tracker.push(start + Duration::from_millis(96), point(px(0.), px(54.))); + let velocity = velocity_tracker.velocity(); + assert!( + velocity.y < 200., + "pre-pause motion leaked into the estimate: {} px/s", + velocity.y + ); + } + + #[test] + fn claimed_touch_drag_emits_phased_stream_without_pan_or_tap() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); + let Some(RecognizedTouchGesture::TouchDrag(started)) = recognizer.offer_touch_drag(touch) + else { + panic!("expected touch drag"); + }; + assert_eq!(started.phase, TouchPhase::Started); + assert_eq!(started.start_position, point(px(10.), px(20.))); + recognizer.resolve_touch_drag(true); + + let moved = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 40., 50.), + now + Duration::from_millis(10), + ); + let [RecognizedTouchGesture::TouchDrag(moved)] = moved.as_slice() else { + panic!("expected moved touch drag, got {moved:?}"); + }; + assert_eq!(moved.phase, TouchPhase::Moved); + assert_eq!(moved.position, point(px(40.), px(50.))); + + let ended = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 45., 55.), + now + Duration::from_millis(20), + ); + let [RecognizedTouchGesture::TouchDrag(ended)] = ended.as_slice() else { + panic!("expected ended touch drag, got {ended:?}"); + }; + assert_eq!(ended.phase, TouchPhase::Ended); + assert_eq!(ended.position, point(px(45.), px(55.))); + } + + #[test] + fn unclaimed_touch_drag_remains_a_pan_candidate() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); + assert!(recognizer.offer_touch_drag(touch).is_some()); + recognizer.resolve_touch_drag(false); + + let moved = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 20., 0.), + now + Duration::from_millis(10), + ); + assert!(matches!( + moved.as_slice(), + [RecognizedTouchGesture::Scroll(ScrollWheelEvent { + touch_phase: TouchPhase::Started, + .. + })] + )); + } + + #[test] + fn claimed_long_press_emits_phased_stream_without_tap() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); + let Some(RecognizedTouchGesture::LongPress(started)) = recognizer.offer_long_press(touch) + else { + panic!("expected long press"); + }; + assert_eq!(started.phase, TouchPhase::Started); + assert_eq!(started.start_position, point(px(10.), px(20.))); + recognizer.resolve_long_press(true); + + let moved = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 12., 21.), + now + Duration::from_millis(510), + ); + let [RecognizedTouchGesture::LongPress(moved)] = moved.as_slice() else { + panic!("expected moved long press, got {moved:?}"); + }; + assert_eq!(moved.phase, TouchPhase::Moved); + + let ended = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 12., 21.), + now + Duration::from_millis(520), + ); + let [RecognizedTouchGesture::LongPress(ended)] = ended.as_slice() else { + panic!("expected ended long press, got {ended:?}"); + }; + assert_eq!(ended.phase, TouchPhase::Ended); + } + + #[test] + fn unclaimed_long_press_remains_a_tap_candidate() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now); + assert!(recognizer.offer_long_press(touch).is_some()); + recognizer.resolve_long_press(false); + + let ended = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Ended, 10., 20.), + now + Duration::from_millis(510), + ); + assert!(matches!( + ended.as_slice(), + [RecognizedTouchGesture::Tap { .. }] + )); + } + + #[test] + fn unclaimed_long_press_can_still_become_a_pan() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); + assert!(recognizer.offer_long_press(touch).is_some()); + recognizer.resolve_long_press(false); + + let moved = recognizer.handle_event_at( + &touch_event(touch, TouchPhase::Moved, 20., 0.), + now + Duration::from_millis(510), + ); + assert!(matches!( + moved.as_slice(), + [RecognizedTouchGesture::Scroll(ScrollWheelEvent { + touch_phase: TouchPhase::Started, + .. + })] + )); + } + + #[test] + fn long_press_offer_is_one_shot_and_specific_to_pending_touch() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 0., 0.)); + + assert!( + recognizer + .handle_event(&touch_event(TouchId(2), TouchPhase::Moved, 20., 0.)) + .is_empty() + ); + assert!(recognizer.offer_long_press(TouchId(2)).is_none()); + assert!(recognizer.offer_long_press(touch).is_some()); + assert!(recognizer.offer_long_press(touch).is_none()); + } + + #[test] + fn long_press_cannot_be_offered_after_pending_touch_resolves() { + for phase in [TouchPhase::Ended, TouchPhase::Cancelled, TouchPhase::Moved] { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + let now = Instant::now(); + recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now); + let position = if phase == TouchPhase::Moved { 20. } else { 0. }; + recognizer.handle_event_at( + &touch_event(touch, phase, position, 0.), + now + Duration::from_millis(10), + ); + assert!(recognizer.offer_long_press(touch).is_none()); + } + } + + #[test] + fn claimed_long_press_emits_cancelled_for_its_touch_only() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.)); + assert!(recognizer.offer_long_press(touch).is_some()); + recognizer.resolve_long_press(true); + + assert!( + recognizer + .handle_event(&touch_event(TouchId(2), TouchPhase::Cancelled, 9., 9.)) + .is_empty() + ); + let cancelled = recognizer.handle_event(&touch_event(touch, TouchPhase::Cancelled, 6., 7.)); + let [RecognizedTouchGesture::LongPress(cancelled)] = cancelled.as_slice() else { + panic!("expected cancelled long press, got {cancelled:?}"); + }; + assert_eq!(cancelled.phase, TouchPhase::Cancelled); + assert_eq!(cancelled.start_position, point(px(4.), px(5.))); + assert_eq!(cancelled.position, point(px(6.), px(7.))); + } + + #[test] + fn unrelated_touch_cannot_end_or_cancel_pending_touch() { + for phase in [TouchPhase::Ended, TouchPhase::Cancelled] { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let touch = TouchId(1); + recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.)); + + assert!( + recognizer + .handle_event(&touch_event(TouchId(2), phase, 9., 9.)) + .is_empty() + ); + assert!(recognizer.offer_long_press(touch).is_some()); + } + } + + #[test] + fn completed_touch_id_cannot_claim_replacement_touch() { + let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default()); + let completed_touch = TouchId(1); + let replacement_touch = TouchId(2); + recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Started, 0., 0.)); + recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Cancelled, 0., 0.)); + recognizer.handle_event(&touch_event(replacement_touch, TouchPhase::Started, 5., 5.)); + + assert!(recognizer.offer_long_press(completed_touch).is_none()); + assert!(recognizer.offer_long_press(replacement_touch).is_some()); + } + + fn touch_event(id: TouchId, phase: TouchPhase, x: f32, y: f32) -> TouchEvent { + TouchEvent { + id, + phase, + position: point(px(x), px(y)), + predicted_position: None, + force: None, + } + } +} diff --git a/vendor/gpui-pre/src/global.rs b/vendor/gpui-pre/src/global.rs new file mode 100644 index 0000000..a16934c --- /dev/null +++ b/vendor/gpui-pre/src/global.rs @@ -0,0 +1,75 @@ +use crate::{App, BorrowAppContext}; + +/// A marker trait for types that can be stored in GPUI's global state. +/// +/// This trait exists to provide type-safe access to globals by ensuring only +/// types that implement [`Global`] can be used with the accessor methods. For +/// example, trying to access a global with a type that does not implement +/// [`Global`] will result in a compile-time error. +/// +/// Implement this on types you want to store in the context as a global. +/// +/// ## Restricting Access to Globals +/// +/// In some situations you may need to store some global state, but want to +/// restrict access to reading it or writing to it. +/// +/// In these cases, Rust's visibility system can be used to restrict access to +/// a global value. For example, you can create a private struct that implements +/// [`Global`] and holds the global state. Then create a newtype struct that wraps +/// the global type and create custom accessor methods to expose the desired subset +/// of operations. +pub trait Global: 'static { + // This trait is intentionally left empty, by virtue of being a marker trait. + // + // Use additional traits with blanket implementations to attach functionality + // to types that implement `Global`. +} + +/// A trait for reading a global value from the context. +pub trait ReadGlobal { + /// Returns the global instance of the implementing type. + /// + /// Panics if a global for that type has not been assigned. + fn global(cx: &App) -> &Self; +} + +impl ReadGlobal for T { + fn global(cx: &App) -> &Self { + cx.global::() + } +} + +/// A trait for updating a global value in the context. +pub trait UpdateGlobal { + /// Updates the global instance of the implementing type using the provided closure. + /// + /// This method provides the closure with mutable access to the context and the global simultaneously. + fn update_global(cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut Self, &mut C) -> R; + + /// Set the global instance of the implementing type. + fn set_global(cx: &mut C, global: Self) + where + C: BorrowAppContext; +} + +impl UpdateGlobal for T { + #[track_caller] + fn update_global(cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut Self, &mut C) -> R, + { + cx.update_global(update) + } + + fn set_global(cx: &mut C, global: Self) + where + C: BorrowAppContext, + { + cx.set_global(global) + } +} diff --git a/vendor/gpui-pre/src/gpui.rs b/vendor/gpui-pre/src/gpui.rs new file mode 100644 index 0000000..d4da5aa --- /dev/null +++ b/vendor/gpui-pre/src/gpui.rs @@ -0,0 +1,402 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] +#![allow(clippy::type_complexity)] // Not useful, GPUI makes heavy use of callbacks +#![allow(clippy::collapsible_else_if)] // False positives in platform specific code +#![allow(unused_mut)] // False positives in platform specific code + +extern crate self as gpui; +#[macro_use] +mod action; +mod app; + +mod arena; +mod asset_cache; +mod assets; +mod bounds_tree; +mod color; +/// The default colors used by GPUI. +pub mod colors; +#[cfg(feature = "profiler")] +mod debug_overlay; +mod element; +mod elements; +mod executor; +mod platform_scheduler; +pub(crate) use platform_scheduler::PlatformScheduler; +mod geometry; +mod gestures; +mod global; +mod input; +mod inspector; +mod interactive; +mod key_dispatch; +mod keymap; +mod path_builder; +mod platform; +pub mod prelude; +/// Profiling utilities for task, frame, and thread performance tracking. +pub mod profiler; +#[cfg(any( + test, + target_os = "windows", + target_os = "linux", + target_family = "wasm", + feature = "test-support", + feature = "bench-support" +))] +#[expect(missing_docs)] +pub mod queue; +/// A seeded, mutable element tree for benchmarks and tests of GPUI rendering. +#[cfg(any(test, feature = "bench-support"))] +pub mod randomized_element_tree; +mod scene; +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +mod seeds; +mod shared_uri; +mod spring; +mod style; +mod styled; +mod subscription; +mod svg_renderer; +mod tab_stop; +mod taffy; +#[cfg(any(test, feature = "test-support"))] +pub mod test; +mod text_system; +mod util; +mod view; +mod window; + +#[cfg(any(test, feature = "test-support"))] +pub use proptest; +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +pub use seeds::calculate_seeds; + +#[cfg(doc)] +pub mod _accessibility; +#[cfg(doc)] +pub mod _ownership_and_data_flow; + +/// Do not touch, here be dragons for use by gpui_macros and such. +#[doc(hidden)] +pub mod private { + pub use anyhow; + pub use inventory; + pub use rand; + pub use schemars; + pub use serde; + pub use serde_json; +} + +mod seal { + /// A mechanism for restricting implementations of a trait to only those in GPUI. + /// See: + pub trait Sealed {} +} + +pub use accesskit; +pub use accesskit::Action as AccessibleAction; +pub use accesskit::{Orientation, Role, Toggled}; +pub use action::*; +pub use anyhow::Result; +pub use app::*; +pub(crate) use arena::*; +pub use asset_cache::*; +pub use assets::*; +pub use color::*; +pub use ctor::ctor; +#[cfg(feature = "profiler")] +pub use debug_overlay::*; +pub use element::*; +pub use elements::*; +pub use executor::*; +pub use geometry::*; +pub use gestures::*; +pub use global::*; +pub use gpui_macros::{ + AppContext, IntoElement, Render, VisualContext, bench, property_test, register_action, test, +}; +pub use spring::*; + +/// Defines a Criterion benchmark group for benchmarks annotated with [`gpui::bench`]. +/// +/// This mirrors `criterion::criterion_group!`, but the group measures with the +/// `gpui::BenchMeasurement` configured by `BENCH_MEASUREMENT` (see +/// `BenchMeasurement::from_env`). By default Criterion analyzes wall time +/// while retired instructions, cycles, IPC, context switches, and the other +/// counters this machine supports are printed per iteration in the GPUI bench +/// report. `BENCH_MEASUREMENT=instructions` makes Criterion analyze +/// process-wide instructions instead, `foreground-instructions` the benchmark +/// thread's alone, and `wall-time` disables all counters. A +/// `config = ...` expression may set any other Criterion option; its +/// measurement is replaced. To measure with something else, call +/// `criterion::criterion_group!` directly with +/// `config = criterion::Criterion::default().with_measurement(gpui::BenchMeasurement::new(...))`. +/// +/// [`gpui::bench`]: crate::bench +#[macro_export] +macro_rules! bench_group { + (name = $name:ident; config = $config:expr; targets = $($target:path),+ $(,)?) => { + criterion::criterion_group! { + name = $name; + config = ($config).with_measurement($crate::BenchMeasurement::from_env_or_exit()); + targets = $($target),+ + } + }; + ($name:ident, $($target:path),+ $(,)?) => { + $crate::bench_group! { + name = $name; + config = criterion::Criterion::default(); + targets = $($target),+ + } + }; +} + +/// Defines the entry point for GPUI Criterion benchmark groups. +/// +/// This mirrors `criterion::criterion_main!` so GPUI benchmark files can keep the +/// same shape as ordinary Criterion benchmarks. It also installs +/// [`CountingAllocator`] as the binary's global allocator, so reports include +/// heap allocations per iteration. It wraps the system allocator by default; +/// `allocator = Path;` wraps another unit-struct allocator instead, e.g. to +/// compare allocators, which pay the same small counting cost: +/// +/// ```ignore +/// gpui::bench_main!(allocator = mimalloc::MiMalloc; benches); +/// ``` +/// +/// `allocator = none;` installs no global allocator, leaving Rust's default and +/// dropping the allocation metrics from reports. +#[macro_export] +macro_rules! bench_main { + (allocator = none; $($groups:tt)*) => { + criterion::criterion_main!($($groups)*); + }; + (allocator = $allocator:path; $($groups:tt)*) => { + #[global_allocator] + static GPUI_BENCH_ALLOCATOR: $crate::CountingAllocator<$allocator> = + $crate::CountingAllocator::new($allocator); + + criterion::criterion_main!($($groups)*); + }; + ($($groups:tt)*) => { + $crate::bench_main!(allocator = ::std::alloc::System; $($groups)*); + }; +} +pub use gpui_shared_string::*; +pub use gpui_util::arc_cow::ArcCow; +pub use http_client; +pub use input::*; +pub use inspector::*; +pub use interactive::*; +use key_dispatch::*; +pub use keymap::*; +pub use path_builder::*; +pub use platform::*; +pub use profiler::*; +#[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))] +pub use queue::{PriorityQueueReceiver, PriorityQueueSender}; +pub use refineable::*; +pub use scene::*; +pub use shared_uri::*; +use std::{any::Any, future::Future}; +pub use style::*; +pub use styled::*; +pub use subscription::*; +pub use svg_renderer::*; +pub(crate) use tab_stop::*; +use taffy::TaffyLayoutEngine; +pub use taffy::{AvailableSpace, LayoutId}; +#[cfg(any(test, feature = "test-support"))] +pub use test::*; +pub use text_system::*; +pub use util::{FutureExt, Timeout}; +pub use view::*; +pub use window::*; + +#[cfg(not(target_family = "wasm"))] +pub use pollster::block_on; + +/// The context trait, allows the different contexts in GPUI to be used +/// interchangeably for certain operations. +pub trait AppContext { + /// Create a new entity in the app context. + #[expect( + clippy::wrong_self_convention, + reason = "`App::new` is an ubiquitous function for creating entities" + )] + fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity; + + /// Reserve a slot for a entity to be inserted later. + /// The returned [Reservation] allows you to obtain the [EntityId] for the future entity. + fn reserve_entity(&mut self) -> Reservation; + + /// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`]. + /// + /// [`reserve_entity`]: Self::reserve_entity + fn insert_entity( + &mut self, + reservation: Reservation, + build_entity: impl FnOnce(&mut Context) -> T, + ) -> Entity; + + /// Update a entity in the app context. + fn update_entity( + &mut self, + handle: &Entity, + update: impl FnOnce(&mut T, &mut Context) -> R, + ) -> R + where + T: 'static; + + /// Update a entity in the app context. + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> GpuiBorrow<'a, T> + where + T: 'static; + + /// Read a entity from the app context. + fn read_entity(&self, handle: &Entity, read: impl FnOnce(&T, &App) -> R) -> R + where + T: 'static; + + /// Update a window for the given handle. + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Result + where + F: FnOnce(AnyView, &mut Window, &mut App) -> T; + + /// Run `f` against the entity's *current* window — the most recently + /// rendered window that referenced the entity. Returns `None` if the + /// entity has no current window or that window is unavailable. See + /// [`App::with_window`] for the underlying lookup. + fn with_window( + &mut self, + entity_id: EntityId, + f: impl FnOnce(&mut Window, &mut App) -> R, + ) -> Option; + + /// Read a window off of the application context. + fn read_window( + &self, + window: &WindowHandle, + read: impl FnOnce(Entity, &App) -> R, + ) -> Result + where + T: 'static; + + /// Spawn a future on a background thread + fn background_spawn(&self, future: impl Future + Send + 'static) -> Task + where + R: Send + 'static; + + /// Read a global from this app context + fn read_global(&self, callback: impl FnOnce(&G, &App) -> R) -> R + where + G: Global; +} + +/// Returned by [Context::reserve_entity] to later be passed to [Context::insert_entity]. +/// Allows you to obtain the [EntityId] for a entity before it is created. +pub struct Reservation(pub(crate) Slot); + +impl Reservation { + /// Returns the [EntityId] that will be associated with the entity once it is inserted. + pub fn entity_id(&self) -> EntityId { + self.0.entity_id() + } +} + +/// This trait is used for the different visual contexts in GPUI that +/// require a window to be present. +pub trait VisualContext: AppContext { + /// The result type for window operations. + type Result; + + /// Returns the handle of the window associated with this context. + fn window_handle(&self) -> AnyWindowHandle; + + /// Update a view with the given callback + fn update_window_entity( + &mut self, + entity: &Entity, + update: impl FnOnce(&mut T, &mut Window, &mut Context) -> R, + ) -> Self::Result; + + /// Create a new entity, with access to `Window`. + fn new_window_entity( + &mut self, + build_entity: impl FnOnce(&mut Window, &mut Context) -> T, + ) -> Self::Result>; + + /// Replace the root view of a window with a new view. + fn replace_root_view( + &mut self, + build_view: impl FnOnce(&mut Window, &mut Context) -> V, + ) -> Self::Result> + where + V: 'static + Render; + + /// Focus a entity in the window, if it implements the [`Focusable`] trait. + fn focus(&mut self, entity: &Entity) -> Self::Result<()> + where + V: Focusable; +} + +/// A trait for tying together the types of a GPUI entity and the events it can +/// emit. +pub trait EventEmitter: 'static {} + +/// A helper trait for auto-implementing certain methods on contexts that +/// can be used interchangeably. +pub trait BorrowAppContext { + /// Set a global value on the context. + fn set_global(&mut self, global: T); + /// Updates the global state of the given type. + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global; + /// Updates the global state of the given type, creating a default if it didn't exist before. + fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default; +} + +impl BorrowAppContext for C +where + C: std::borrow::BorrowMut, +{ + fn set_global(&mut self, global: G) { + self.borrow_mut().set_global(global) + } + + #[track_caller] + fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global, + { + let mut global = self.borrow_mut().lease_global::(); + let result = f(&mut global, self); + self.borrow_mut().end_global_lease(global); + result + } + + fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default, + { + self.borrow_mut().default_global::(); + self.update_global(f) + } +} + +/// Information about the GPU GPUI is running on. +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +pub struct GpuSpecs { + /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. + pub is_software_emulated: bool, + /// The name of the device, as reported by Vulkan. + pub device_name: String, + /// The name of the driver, as reported by Vulkan. + pub driver_name: String, + /// Further information about the driver, as reported by Vulkan. + pub driver_info: String, +} diff --git a/vendor/gpui-pre/src/input.rs b/vendor/gpui-pre/src/input.rs new file mode 100644 index 0000000..4167aa0 --- /dev/null +++ b/vendor/gpui-pre/src/input.rs @@ -0,0 +1,485 @@ +use crate::{ + App, Bounds, ClipboardItem, Context, Entity, InputHandler, Pixels, TextInputConfiguration, + UTF16Selection, Window, +}; +use std::ops::Range; + +/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. +/// +/// Once your view implements this trait, you can use it to construct an [`ElementInputHandler`]. +/// This input handler can then be assigned during paint by calling [`Window::handle_input`]. +/// +/// See [`InputHandler`] for details on how to implement each method. +pub trait EntityInputHandler: 'static + Sized { + /// See [`InputHandler::text_for_range`] for details + fn text_for_range( + &mut self, + range: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut Context, + ) -> Option; + + /// See [`InputHandler::selected_text_range`] for details + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option; + + /// See [`InputHandler::marked_text_range`] for details + fn marked_text_range( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Option>; + + /// See [`InputHandler::unmark_text`] for details + fn unmark_text(&mut self, window: &mut Window, cx: &mut Context); + + /// See [`InputHandler::paste`] for details + fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut Context) { + if let Some(text) = item.text() { + self.replace_text_in_range(None, &text, window, cx); + } + } + + /// See [`InputHandler::replace_text_in_range`] for details + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ); + + /// See [`InputHandler::replace_and_mark_text_in_range`] for details + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut Context, + ); + + /// See [`InputHandler::bounds_for_range`] for details + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: Bounds, + window: &mut Window, + cx: &mut Context, + ) -> Option>; + + /// See [`InputHandler::character_index_for_point`] for details + fn character_index_for_point( + &mut self, + point: crate::Point, + window: &mut Window, + cx: &mut Context, + ) -> Option; + + /// See [`InputHandler::set_selected_text_range`] for details + fn set_selected_text_range( + &mut self, + _range_utf16: Range, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + /// See [`InputHandler::text_length_utf16`] for details + fn text_length_utf16( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + /// See [`InputHandler::accepts_text_input`] for details + fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { + true + } + + /// See [`InputHandler::text_input_configuration`] for details + fn text_input_configuration( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> TextInputConfiguration { + TextInputConfiguration::default() + } + + /// See [`InputHandler::text_input_editable_range`] for details + fn text_input_editable_range( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } +} + +/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`] +/// with an instance during your element's paint. +pub struct ElementInputHandler { + view: Entity, + element_bounds: Bounds, +} + +impl ElementInputHandler { + /// Used in [`Element::paint`][element_paint] with the element's bounds, a `Window`, and a `App` context. + /// + /// [element_paint]: crate::Element::paint + pub fn new(element_bounds: Bounds, view: Entity) -> Self { + ElementInputHandler { + view, + element_bounds, + } + } +} + +impl InputHandler for ElementInputHandler { + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.selected_text_range(ignore_disabled_input, window, cx) + }) + } + + fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option> { + self.view + .update(cx, |view, cx| view.marked_text_range(window, cx)) + } + + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.text_for_range(range_utf16, adjusted_range, window, cx) + }) + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + window: &mut Window, + cx: &mut App, + ) { + self.view.update(cx, |view, cx| { + view.replace_text_in_range(replacement_range, text, window, cx) + }); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut App, + ) { + self.view.update(cx, |view, cx| { + view.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + window, + cx, + ) + }); + } + + fn unmark_text(&mut self, window: &mut Window, cx: &mut App) { + self.view + .update(cx, |view, cx| view.unmark_text(window, cx)); + } + + fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) { + self.view + .update(cx, |view, cx| view.paste(item, window, cx)); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + window: &mut Window, + cx: &mut App, + ) -> Option> { + self.view.update(cx, |view, cx| { + view.bounds_for_range(range_utf16, self.element_bounds, window, cx) + }) + } + + fn character_index_for_point( + &mut self, + point: crate::Point, + window: &mut Window, + cx: &mut App, + ) -> Option { + self.view.update(cx, |view, cx| { + view.character_index_for_point(point, window, cx) + }) + } + + fn set_selected_text_range( + &mut self, + range_utf16: Range, + window: &mut Window, + cx: &mut App, + ) { + self.view.update(cx, |view, cx| { + view.set_selected_text_range(range_utf16, window, cx) + }) + } + + fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { + Some(self.element_bounds) + } + + fn text_length_utf16(&mut self, window: &mut Window, cx: &mut App) -> Option { + self.view + .update(cx, |view, cx| view.text_length_utf16(window, cx)) + } + + fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { + self.view + .update(cx, |view, cx| view.accepts_text_input(window, cx)) + } + + fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool { + self.view + .update(cx, |view, cx| view.accepts_text_input(window, cx)) + } + + fn text_input_configuration( + &mut self, + window: &mut Window, + cx: &mut App, + ) -> TextInputConfiguration { + self.view + .update(cx, |view, cx| view.text_input_configuration(window, cx)) + } + + fn text_input_editable_range( + &mut self, + window: &mut Window, + cx: &mut App, + ) -> Option> { + self.view + .update(cx, |view, cx| view.text_input_editable_range(window, cx)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AnyWindowHandle, AppContext as _, FocusHandle, InteractiveElement as _, IntoElement, + ParentElement as _, Render, Styled as _, TestAppContext, TextInputAction, + TextInputStateChange, canvas, div, + }; + + #[gpui::test] + fn text_input_configuration_and_focus_state_are_forwarded_on_change(cx: &mut TestAppContext) { + let custom = TextInputConfiguration { + autocorrect: true, + input_action: TextInputAction::Send, + ..Default::default() + }; + let window = cx.add_window({ + let custom = custom.clone(); + move |_, cx| ConfigurationTestView { + focus_handle: cx.focus_handle(), + configuration: custom, + } + }); + let view = window.root(cx).unwrap(); + let test_window = cx.test_window(window.into()); + let window = AnyWindowHandle::from(window); + let draw = |cx: &mut TestAppContext| { + cx.update_window(window, |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + }; + + // Nothing is focused, so the platform learns the default configuration. + draw(cx); + assert_eq!( + test_window.text_input_configurations(), + vec![TextInputConfiguration::default()] + ); + assert!(test_window.text_input_state_changes().is_empty()); + + // Focusing the view routes its configuration to the platform. + cx.update_window(window, |_, window, cx| { + let focus_handle = view.read(cx).focus_handle.clone(); + window.focus(&focus_handle, cx); + }) + .unwrap(); + draw(cx); + assert_eq!( + test_window.text_input_configurations(), + vec![TextInputConfiguration::default(), custom.clone()] + ); + assert_eq!( + test_window.text_input_state_changes(), + vec![TextInputStateChange::FocusGained] + ); + + // Redrawing without a change forwards nothing. + draw(cx); + assert_eq!(test_window.text_input_configurations().len(), 2); + assert_eq!(test_window.text_input_state_changes().len(), 1); + + // Changing the configuration forwards the new value. + let updated = TextInputConfiguration { + suggestions: true, + ..custom + }; + view.update(cx, { + let updated = updated.clone(); + |view, cx| { + view.configuration = updated; + cx.notify(); + } + }); + draw(cx); + assert_eq!( + test_window.text_input_configurations().last(), + Some(&updated) + ); + assert_eq!(test_window.text_input_configurations().len(), 3); + assert_eq!(test_window.text_input_state_changes().len(), 1); + + // Losing focus reverts the platform to the default configuration. + cx.update_window(window, |_, window, cx| window.blur(cx)) + .unwrap(); + draw(cx); + assert_eq!( + test_window.text_input_configurations().last(), + Some(&TextInputConfiguration::default()) + ); + assert_eq!(test_window.text_input_configurations().len(), 4); + assert_eq!( + test_window.text_input_state_changes(), + vec![ + TextInputStateChange::FocusGained, + TextInputStateChange::FocusLost + ] + ); + } + + struct ConfigurationTestView { + focus_handle: FocusHandle, + configuration: TextInputConfiguration, + } + + impl Render for ConfigurationTestView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + let focus_handle = self.focus_handle.clone(); + div().size_full().track_focus(&self.focus_handle).child( + canvas( + |_, _, _| {}, + move |bounds, _, window, cx| { + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, view), + cx, + ); + }, + ) + .size_full(), + ) + } + } + + impl EntityInputHandler for ConfigurationTestView { + fn text_for_range( + &mut self, + _range: std::ops::Range, + _adjusted_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) {} + + fn replace_text_in_range( + &mut self, + _range: Option>, + _text: &str, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + fn replace_and_mark_text_in_range( + &mut self, + _range: Option>, + _new_text: &str, + _new_selected_range: Option>, + _window: &mut Window, + _cx: &mut Context, + ) { + } + + fn bounds_for_range( + &mut self, + _range_utf16: std::ops::Range, + _element_bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _point: crate::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + None + } + + fn text_input_configuration( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> TextInputConfiguration { + self.configuration.clone() + } + } +} diff --git a/vendor/gpui-pre/src/inspector.rs b/vendor/gpui-pre/src/inspector.rs new file mode 100644 index 0000000..d353690 --- /dev/null +++ b/vendor/gpui-pre/src/inspector.rs @@ -0,0 +1,271 @@ +/// A unique identifier for an element that can be inspected. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct InspectorElementId { + /// Stable part of the ID. + #[cfg(any(feature = "inspector", debug_assertions))] + pub path: std::rc::Rc, + /// Disambiguates elements that have the same path. + #[cfg(any(feature = "inspector", debug_assertions))] + pub instance_id: usize, +} + +impl Into for &InspectorElementId { + fn into(self) -> InspectorElementId { + self.clone() + } +} + +#[cfg(any(feature = "inspector", debug_assertions))] +pub use conditional::*; + +#[cfg(any(feature = "inspector", debug_assertions))] +mod conditional { + use super::*; + use crate::{AnyElement, App, Context, Empty, IntoElement, Render, Window}; + use collections::{FxHashMap, TypeIdHashMap, hash_map::Entry}; + use std::any::{Any, TypeId}; + + /// `GlobalElementId` qualified by source location of element construction. + #[derive(Debug, Eq, PartialEq, Hash)] + pub struct InspectorElementPath { + /// The path to the nearest ancestor element that has an `ElementId`. + #[cfg(any(feature = "inspector", debug_assertions))] + pub global_id: crate::GlobalElementId, + /// Source location where this element was constructed. + #[cfg(any(feature = "inspector", debug_assertions))] + pub source_location: &'static std::panic::Location<'static>, + } + + impl Clone for InspectorElementPath { + fn clone(&self) -> Self { + Self { + global_id: self.global_id.clone(), + source_location: self.source_location, + } + } + } + + impl Into for &InspectorElementPath { + fn into(self) -> InspectorElementPath { + self.clone() + } + } + + /// Function set on `App` to render the inspector UI. + pub type InspectorRenderer = + Box) -> AnyElement>; + + /// Manages inspector state - which element is currently selected and whether the inspector is + /// in picking mode. + pub struct Inspector { + active_element: Option, + pub(crate) pick_depth: Option, + renderers: TypeIdHashMap, + } + + struct InspectedElement { + id: InspectorElementId, + states: TypeIdHashMap>, + } + + impl InspectedElement { + fn new(id: InspectorElementId) -> Self { + InspectedElement { + id, + states: Default::default(), + } + } + } + + impl Inspector { + pub(crate) fn new() -> Self { + Self { + active_element: None, + pick_depth: Some(0.0), + renderers: TypeIdHashMap::default(), + } + } + + pub(crate) fn select(&mut self, id: InspectorElementId, window: &mut Window) { + self.set_active_element_id(id, window); + self.pick_depth = None; + } + + pub(crate) fn hover(&mut self, id: InspectorElementId, window: &mut Window) { + if self.is_picking() { + let changed = self.set_active_element_id(id, window); + if changed { + self.pick_depth = Some(0.0); + } + } + } + + pub(crate) fn set_active_element_id( + &mut self, + id: InspectorElementId, + window: &mut Window, + ) -> bool { + let changed = Some(&id) != self.active_element_id(); + if changed { + self.active_element = Some(InspectedElement::new(id)); + window.refresh(); + } + changed + } + + /// ID of the currently hovered or selected element. + pub fn active_element_id(&self) -> Option<&InspectorElementId> { + self.active_element.as_ref().map(|e| &e.id) + } + + pub(crate) fn with_active_element_state( + &mut self, + window: &mut Window, + f: impl FnOnce(&mut Option, &mut Window) -> R, + ) -> R { + let Some(active_element) = &mut self.active_element else { + return f(&mut None, window); + }; + + let type_id = TypeId::of::(); + let mut inspector_state = active_element + .states + .remove(&type_id) + .map(|state| *state.downcast().unwrap()); + + let result = f(&mut inspector_state, window); + + if let Some(inspector_state) = inspector_state { + active_element + .states + .insert(type_id, Box::new(inspector_state)); + } + + result + } + + /// Starts element picking mode, allowing the user to select elements by clicking. + pub fn start_picking(&mut self) { + self.pick_depth = Some(0.0); + } + + /// Returns whether the inspector is currently in picking mode. + pub fn is_picking(&self) -> bool { + self.pick_depth.is_some() + } + + /// Renders elements for all registered inspector states of the active inspector element. + pub fn render_inspector_states( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Vec { + let mut elements = Vec::new(); + if let Some(active_element) = self.active_element.take() { + for (type_id, state) in &active_element.states { + let renderer = match self.renderers.entry(*type_id) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let Some(factory) = cx + .inspector_element_registry + .factories_by_type_id + .remove(type_id) + else { + continue; + }; + let renderer = factory(window, cx); + cx.inspector_element_registry + .factories_by_type_id + .insert(*type_id, factory); + entry.insert(renderer) + } + }; + elements.push(renderer( + active_element.id.clone(), + state.as_ref(), + window, + cx, + )); + } + + self.active_element = Some(active_element); + } + + elements + } + } + + impl Render for Inspector { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if let Some(inspector_renderer) = cx.inspector_renderer.take() { + let result = inspector_renderer(self, window, cx); + cx.inspector_renderer = Some(inspector_renderer); + result + } else { + Empty.into_any_element() + } + } + } + + #[derive(Default)] + pub(crate) struct InspectorElementRegistry { + factories_by_type_id: + FxHashMap InspectorElementRenderer>>, + } + + impl InspectorElementRegistry { + pub fn register( + &mut self, + factory: impl 'static + Fn(&mut Window, &mut App) -> F, + ) where + F: 'static + FnMut(InspectorElementId, &T, &mut Window, &mut App) -> R, + { + self.factories_by_type_id.insert( + TypeId::of::(), + Box::new(move |window, cx| { + let mut renderer = factory(window, cx); + Box::new(move |id, value, window, cx| { + let value = value + .downcast_ref() + .expect("registered inspector state type"); + renderer(id, value, window, cx).into_any_element() + }) + }), + ); + } + } + + type InspectorElementRenderer = + Box AnyElement>; +} + +/// Provides definitions used by `#[derive_inspector_reflection]`. +#[cfg(any(feature = "inspector", debug_assertions))] +pub mod inspector_reflection { + use std::any::Any; + + /// Reification of a function that has the signature `fn some_fn(T) -> T`. Provides the name, + /// documentation, and ability to invoke the function. + #[derive(Clone, Copy)] + pub struct FunctionReflection { + /// The name of the function + pub name: &'static str, + /// The method + pub function: fn(Box) -> Box, + /// Documentation for the function + pub documentation: Option<&'static str>, + /// `PhantomData` for the type of the argument and result + pub _type: std::marker::PhantomData, + } + + impl FunctionReflection { + /// Invoke this method on a value and return the result. + pub fn invoke(&self, value: T) -> T { + let boxed = Box::new(value) as Box; + let result = (self.function)(boxed); + *result + .downcast::() + .expect("Type mismatch in reflection invoke") + } + } +} diff --git a/vendor/gpui-pre/src/interactive.rs b/vendor/gpui-pre/src/interactive.rs new file mode 100644 index 0000000..913cf1b --- /dev/null +++ b/vendor/gpui-pre/src/interactive.rs @@ -0,0 +1,971 @@ +use crate::{ + Bounds, Capslock, Context, Empty, IntoElement, Keystroke, LongPressEvent, Modifiers, Pixels, + Point, Render, TouchDragEvent, Window, point, seal::Sealed, +}; +use smallvec::SmallVec; +use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; + +/// An event from a platform input source. +pub trait InputEvent: Sealed + 'static { + /// Convert this event into the platform input enum. + fn to_platform_input(self) -> PlatformInput; +} + +/// A key event from the platform. +pub trait KeyEvent: InputEvent {} + +/// A mouse event from the platform. +pub trait MouseEvent: InputEvent {} + +/// A gesture event from the platform. +pub trait GestureEvent: InputEvent {} + +/// The key down event equivalent for the platform. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct KeyDownEvent { + /// The keystroke that was generated. + pub keystroke: Keystroke, + + /// Whether the key is currently held down. + pub is_held: bool, + + /// Whether to prefer character input over keybindings for this keystroke. + /// In some cases, like AltGr on Windows, modifiers are significant for character input. + pub prefer_character_input: bool, +} + +impl Sealed for KeyDownEvent {} +impl InputEvent for KeyDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyDown(self) + } +} +impl KeyEvent for KeyDownEvent {} + +/// The key up event equivalent for the platform. +#[derive(Clone, Debug)] +pub struct KeyUpEvent { + /// The keystroke that was released. + pub keystroke: Keystroke, +} + +impl Sealed for KeyUpEvent {} +impl InputEvent for KeyUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyUp(self) + } +} +impl KeyEvent for KeyUpEvent {} + +/// The modifiers changed event equivalent for the platform. +#[derive(Clone, Debug, Default)] +pub struct ModifiersChangedEvent { + /// The new state of the modifier keys + pub modifiers: Modifiers, + /// The new state of the capslock key + pub capslock: Capslock, +} + +impl Sealed for ModifiersChangedEvent {} +impl InputEvent for ModifiersChangedEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ModifiersChanged(self) + } +} +impl KeyEvent for ModifiersChangedEvent {} + +impl Deref for ModifiersChangedEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The phase of a touch motion event. +/// Based on the winit enum of the same name. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TouchPhase { + /// The touch started. + Started, + /// The touch event is moving. + #[default] + Moved, + /// The touch phase has ended + Ended, + /// The touch was cancelled: the system took it and it will not end + /// normally. Consumers must fully unwind any in-progress interaction, + /// treating the touch as if it never committed. + Cancelled, +} + +/// Identifies one touch (finger or stylus contact) for its lifetime, from +/// [`TouchPhase::Started`] through [`TouchPhase::Ended`] or +/// [`TouchPhase::Cancelled`]. +/// +/// The value is opaque and assigned by the platform. A platform window must +/// not reuse an identifier for a later touch. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TouchId(pub u64); + +/// A raw touch event from the platform. +/// +/// +/// Dispatch contract (core implementation pending): a touch is hit-tested +/// once, at [`TouchPhase::Started`], occlusion-aware; all subsequent events +/// for the same [`TouchId`] are delivered to the elements under the starting +/// position, even after the touch moves outside them. +#[derive(Clone, Debug, Default)] +pub struct TouchEvent { + /// Which touch this event belongs to. + pub id: TouchId, + /// The phase of the touch. + pub phase: TouchPhase, + /// The position of the touch in window coordinates. + pub position: Point, + /// Where the platform predicts the touch will be roughly one frame from + /// now, in the same coordinate space as `position`, when the platform + /// offers a prediction for a [`TouchPhase::Moved`] event. + /// + /// Best-effort latency compensation only: it may influence how far a + /// recognized pan scrolls within a frame, but never hit testing, gesture + /// classification, or velocity estimation, and any error it introduces + /// must be corrected by later events for the same touch. + pub predicted_position: Option>, + /// Normalized touch force in `0.0..=1.0`, if the hardware reports it. + pub force: Option, +} + +impl Sealed for TouchEvent {} +impl InputEvent for TouchEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::Touch(self) + } +} + +/// A mouse down event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseDownEvent { + /// Which mouse button was pressed. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was pressed. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, + + /// Whether this is the first, focusing click. + pub first_mouse: bool, +} + +impl Sealed for MouseDownEvent {} +impl InputEvent for MouseDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseDown(self) + } +} +impl MouseEvent for MouseDownEvent {} + +impl MouseDownEvent { + /// Returns true if this mouse up event should focus the element. + pub fn is_focusing(&self) -> bool { + match self.button { + MouseButton::Left => true, + _ => false, + } + } +} + +/// A mouse up event from the platform +#[derive(Clone, Debug, Default)] +pub struct MouseUpEvent { + /// Which mouse button was released. + pub button: MouseButton, + + /// The position of the mouse on the window. + pub position: Point, + + /// The modifiers that were held down when the mouse was released. + pub modifiers: Modifiers, + + /// The number of times the button has been clicked. + pub click_count: usize, +} + +impl Sealed for MouseUpEvent {} +impl InputEvent for MouseUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseUp(self) + } +} + +impl MouseEvent for MouseUpEvent {} + +impl MouseUpEvent { + /// Returns true if this mouse up event should focus the element. + pub fn is_focusing(&self) -> bool { + match self.button { + MouseButton::Left => true, + _ => false, + } + } +} + +/// A click event, generated when a mouse button is pressed and released. +#[derive(Clone, Debug, Default)] +pub struct MouseClickEvent { + /// The mouse event when the button was pressed. + pub down: MouseDownEvent, + + /// The mouse event when the button was released. + pub up: MouseUpEvent, +} + +/// The stage of a pressure click event. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum PressureStage { + /// No pressure. + #[default] + Zero, + /// Normal click pressure. + Normal, + /// High pressure, enough to trigger a force click. + Force, +} + +/// A mouse pressure event from the platform. Generated when a force-sensitive trackpad is pressed hard. +/// Currently only implemented for macOS trackpads. +#[derive(Debug, Clone, Default)] +pub struct MousePressureEvent { + /// Pressure of the current stage as a float between 0 and 1 + pub pressure: f32, + /// The pressure stage of the event. + pub stage: PressureStage, + /// The position of the mouse on the window. + pub position: Point, + /// The modifiers that were held down when the mouse pressure changed. + pub modifiers: Modifiers, +} + +impl Sealed for MousePressureEvent {} +impl InputEvent for MousePressureEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MousePressure(self) + } +} +impl MouseEvent for MousePressureEvent {} + +/// A click event that was generated by a keyboard button being pressed and released. +#[derive(Clone, Debug, Default)] +pub struct KeyboardClickEvent { + /// The keyboard button that was pressed to trigger the click. + pub button: KeyboardButton, + + /// The bounds of the element that was clicked. + pub bounds: Bounds, +} + +/// A click event that was generated by a recognized tap gesture on a touch +/// screen. +#[derive(Clone, Debug, Default)] +pub struct TouchClickEvent { + /// The position of the tap in window coordinates. + pub position: Point, + /// The number of consecutive taps at this location (double tap = 2), + /// analogous to the mouse `click_count`. + pub tap_count: usize, + /// Whether this was a long press rather than a tap. Long presses are + /// touch's secondary activation: they are delivered to aux-click + /// listeners alongside right clicks, not to primary click listeners. + pub long_press: bool, +} + +/// A click event, generated when a mouse button or keyboard button is pressed and released, +/// or when a tap gesture is recognized on a touch screen. +#[derive(Clone, Debug)] +pub enum ClickEvent { + /// A click event trigger by a mouse button being pressed and released. + Mouse(MouseClickEvent), + /// A click event trigger by a keyboard button being pressed and released. + Keyboard(KeyboardClickEvent), + /// A click event triggered by a recognized tap gesture on a touch screen. + Touch(TouchClickEvent), +} + +impl Default for ClickEvent { + fn default() -> Self { + ClickEvent::Keyboard(KeyboardClickEvent::default()) + } +} + +impl ClickEvent { + /// Returns the modifiers that were held during the click event + /// + /// `Keyboard`: The keyboard click events never have modifiers. + /// `Mouse`: Modifiers that were held during the mouse key up event. + pub fn modifiers(&self) -> Modifiers { + match self { + // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default + ClickEvent::Keyboard(_) => Modifiers::default(), + // Click events on the web only reflect the modifiers for the keyup event, + // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138 + // under various combinations of modifiers and keyUp / keyDown events. + ClickEvent::Mouse(event) => event.up.modifiers, + // Touch screens have no modifier keys. + ClickEvent::Touch(_) => Modifiers::default(), + } + } + + /// Returns the position of the click event + /// + /// `Keyboard`: The bottom left corner of the clicked hitbox + /// `Mouse`: The position of the mouse when the button was released. + /// `Touch`: The position of the tap. + pub fn position(&self) -> Point { + match self { + ClickEvent::Keyboard(event) => event.bounds.bottom_left(), + ClickEvent::Mouse(event) => event.up.position, + ClickEvent::Touch(event) => event.position, + } + } + + /// Returns the mouse position of the click event + /// + /// `Keyboard`: None + /// `Mouse`: The position of the mouse when the button was released. + /// `Touch`: None, touches are not mouse input and there is no cursor. + pub fn mouse_position(&self) -> Option> { + match self { + ClickEvent::Keyboard(_) => None, + ClickEvent::Mouse(event) => Some(event.up.position), + ClickEvent::Touch(_) => None, + } + } + + /// Returns if this was a right click + /// + /// `Keyboard`: false + /// `Mouse`: Whether the right button was pressed and released + pub fn is_right_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Right && event.up.button == MouseButton::Right + } + ClickEvent::Touch(_) => false, + } + } + + /// Returns if this was a middle click + /// + /// `Keyboard`: false + /// `Mouse`: Whether the middle button was pressed and released + pub fn is_middle_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Middle && event.up.button == MouseButton::Middle + } + ClickEvent::Touch(_) => false, + } + } + + /// Returns whether the click is a secondary activation, i.e. a context + /// menu trigger: a right click from a mouse (macOS ctrl-clicks arrive + /// already converted to right clicks by the platform layer), or a long + /// press on a touch screen. + pub fn is_secondary(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Right && event.up.button == MouseButton::Right + } + ClickEvent::Touch(event) => event.long_press, + } + } + + /// Returns whether the click was a standard click + /// + /// `Keyboard`: Always true + /// `Mouse`: Left button pressed and released + /// `Touch`: A tap, but not a long press + pub fn standard_click(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(event) => { + event.down.button == MouseButton::Left && event.up.button == MouseButton::Left + } + ClickEvent::Touch(event) => !event.long_press, + } + } + + /// Returns whether the click focused the element + /// + /// `Keyboard`: false, keyboard clicks only work if an element is already focused + /// `Mouse`: Whether this was the first focusing click + /// `Touch`: false, mobile windows are already active when tappable + pub fn first_focus(&self) -> bool { + match self { + ClickEvent::Keyboard(_) => false, + ClickEvent::Mouse(event) => event.down.first_mouse, + ClickEvent::Touch(_) => false, + } + } + + /// Returns the click count of the click event + /// + /// `Keyboard`: Always 1 + /// `Mouse`: Count of clicks from MouseUpEvent + /// `Touch`: Count of consecutive taps + pub fn click_count(&self) -> usize { + match self { + ClickEvent::Keyboard(_) => 1, + ClickEvent::Mouse(event) => event.up.click_count, + ClickEvent::Touch(event) => event.tap_count, + } + } + + /// Returns whether the click event is generated by a keyboard event + pub fn is_keyboard(&self) -> bool { + match self { + ClickEvent::Mouse(_) | ClickEvent::Touch(_) => false, + ClickEvent::Keyboard(_) => true, + } + } +} + +/// An enum representing the keyboard button that was pressed for a click event. +#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)] +pub enum KeyboardButton { + /// Enter key was clicked + #[default] + Enter, + /// Space key was clicked + Space, +} + +/// An enum representing the mouse button that was pressed. +#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)] +pub enum MouseButton { + /// The left mouse button. + #[default] + Left, + + /// The right mouse button. + Right, + + /// The middle mouse button. + Middle, + + /// A navigation button, such as back or forward. + Navigate(NavigationDirection), +} + +impl MouseButton { + /// Get all the mouse buttons in a list. + pub fn all() -> Vec { + vec![ + MouseButton::Left, + MouseButton::Right, + MouseButton::Middle, + MouseButton::Navigate(NavigationDirection::Back), + MouseButton::Navigate(NavigationDirection::Forward), + ] + } +} + +/// A navigation direction, such as back or forward. +#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)] +pub enum NavigationDirection { + /// The back button. + #[default] + Back, + + /// The forward button. + Forward, +} + +/// A mouse move event from the platform. +#[derive(Clone, Debug, Default)] +pub struct MouseMoveEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseMoveEvent {} +impl InputEvent for MouseMoveEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseMove(self) + } +} +impl MouseEvent for MouseMoveEvent {} + +impl MouseMoveEvent { + /// Returns true if the left mouse button is currently held down. + pub fn dragging(&self) -> bool { + self.pressed_button == Some(MouseButton::Left) + } +} + +/// A mouse wheel event from the platform. +#[derive(Clone, Debug, Default)] +pub struct ScrollWheelEvent { + /// The position of the mouse on the window. + pub position: Point, + + /// The change in scroll wheel position for this event. + pub delta: ScrollDelta, + + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, + + /// The phase of the touch event. + pub touch_phase: TouchPhase, +} + +impl Sealed for ScrollWheelEvent {} +impl InputEvent for ScrollWheelEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ScrollWheel(self) + } +} +impl MouseEvent for ScrollWheelEvent {} + +impl Deref for ScrollWheelEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// The scroll delta for a scroll wheel event. +#[derive(Clone, Copy, Debug)] +pub enum ScrollDelta { + /// An exact scroll delta in pixels. + Pixels(Point), + /// An inexact scroll delta in lines. + Lines(Point), +} + +impl Default for ScrollDelta { + fn default() -> Self { + Self::Lines(Default::default()) + } +} + +/// A pinch gesture event from the platform, generated when the user performs +/// a pinch-to-zoom gesture (typically on a trackpad). +/// +#[derive(Clone, Debug, Default)] +pub struct PinchEvent { + /// The position of the pinch center on the window. + pub position: Point, + + /// The zoom delta for this event. + /// Positive values indicate zooming in, negative values indicate zooming out. + /// For example, 0.1 represents a 10% zoom increase. + pub delta: f32, + + /// The modifiers that were held down during the pinch gesture. + pub modifiers: Modifiers, + + /// The phase of the pinch gesture. + pub phase: TouchPhase, +} + +impl Sealed for PinchEvent {} +impl InputEvent for PinchEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::Pinch(self) + } +} +impl GestureEvent for PinchEvent {} +impl MouseEvent for PinchEvent {} + +impl Deref for PinchEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +impl ScrollDelta { + /// Returns true if this is a precise scroll delta in pixels. + pub fn precise(&self) -> bool { + match self { + ScrollDelta::Pixels(_) => true, + ScrollDelta::Lines(_) => false, + } + } + + /// Converts this scroll event into exact pixels. + pub fn pixel_delta(&self, line_height: Pixels) -> Point { + match self { + ScrollDelta::Pixels(delta) => *delta, + ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), + } + } + + /// Combines two scroll deltas into one. + /// If the signs of the deltas are the same (both positive or both negative), + /// the deltas are added together. If the signs are opposite, the second delta + /// (other) is used, effectively overriding the first delta. + pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { + match (self, other) { + (ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => { + let x = if a.x.signum() == b.x.signum() { + a.x + b.x + } else { + b.x + }; + + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; + + ScrollDelta::Pixels(point(x, y)) + } + + (ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => { + let x = if a.x.signum() == b.x.signum() { + a.x + b.x + } else { + b.x + }; + + let y = if a.y.signum() == b.y.signum() { + a.y + b.y + } else { + b.y + }; + + ScrollDelta::Lines(point(x, y)) + } + + _ => other, + } + } +} + +/// A mouse exit event from the platform, generated when the mouse leaves the window. +#[derive(Clone, Debug, Default)] +pub struct MouseExitEvent { + /// The position of the mouse relative to the window. + pub position: Point, + /// The mouse button that was pressed, if any. + pub pressed_button: Option, + /// The modifiers that were held down when the mouse was moved. + pub modifiers: Modifiers, +} + +impl Sealed for MouseExitEvent {} +impl InputEvent for MouseExitEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseExited(self) + } +} + +impl MouseEvent for MouseExitEvent {} + +impl Deref for MouseExitEvent { + type Target = Modifiers; + + fn deref(&self) -> &Self::Target { + &self.modifiers + } +} + +/// A collection of paths from the platform, such as from a file drop. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>); + +impl ExternalPaths { + /// Convert this collection of paths into a slice. + pub fn paths(&self) -> &[PathBuf] { + &self.0 + } +} + +/// Data offered to the platform when an internal drag leaves the window and is +/// promoted to a native drag session. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ExternalDragPayload { + /// Real on-disk paths, handed to the platform as an outbound file drag. + Files(FileDragPaths), +} + +/// Paths handed to the platform for a native file drag. Directory metadata is +/// provided by the caller to avoid querying it when the platform drag starts. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct FileDragPaths(SmallVec<[(PathBuf, bool); 2]>); + +impl FileDragPaths { + /// Creates a native file-drag payload from paths paired with whether each path is a directory. + pub fn new(entries: impl IntoIterator) -> Self { + Self(entries.into_iter().collect()) + } + + /// The dragged paths, each paired with whether it is a directory. + pub fn entries(&self) -> &[(PathBuf, bool)] { + &self.0 + } +} + +impl Render for ExternalPaths { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + // the platform will render icons for the dragged files + Empty + } +} + +/// A file drop event from the platform, generated when files are dragged and dropped onto the window. +#[derive(Debug, Clone)] +pub enum FileDropEvent { + /// The files have entered the window. + Entered { + /// The position of the mouse relative to the window. + position: Point, + /// The paths of the files that are being dragged. + paths: ExternalPaths, + }, + /// The files are being dragged over the window + Pending { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The files have been dropped onto the window. + Submit { + /// The position of the mouse relative to the window. + position: Point, + }, + /// The user has stopped dragging the files over the window. + Exited, + /// The platform-owned drag session has ended. + Ended, +} + +impl Sealed for FileDropEvent {} +impl InputEvent for FileDropEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::FileDrop(self) + } +} +impl MouseEvent for FileDropEvent {} + +/// An enum corresponding to all kinds of platform input events. +#[derive(Clone, Debug)] +pub enum PlatformInput { + /// A key was pressed. + KeyDown(KeyDownEvent), + /// A key was released. + KeyUp(KeyUpEvent), + /// The keyboard modifiers were changed. + ModifiersChanged(ModifiersChangedEvent), + /// The mouse was pressed. + MouseDown(MouseDownEvent), + /// The mouse was released. + MouseUp(MouseUpEvent), + /// Mouse pressure. + MousePressure(MousePressureEvent), + /// The mouse was moved. + MouseMove(MouseMoveEvent), + /// The mouse exited the window. + MouseExited(MouseExitEvent), + /// The scroll wheel was used. + ScrollWheel(ScrollWheelEvent), + /// A pinch gesture was performed. + Pinch(PinchEvent), + /// A long-press gesture recognized from touch input. + LongPress(LongPressEvent), + /// A direct touch drag claimed by an element. + TouchDrag(TouchDragEvent), + /// Files were dragged and dropped onto the window. + FileDrop(FileDropEvent), + /// A raw touch event on a touch screen. + Touch(TouchEvent), +} + +impl PlatformInput { + pub(crate) fn mouse_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown { .. } => None, + PlatformInput::KeyUp { .. } => None, + PlatformInput::ModifiersChanged { .. } => None, + PlatformInput::MouseDown(event) => Some(event), + PlatformInput::MouseUp(event) => Some(event), + PlatformInput::MouseMove(event) => Some(event), + PlatformInput::MousePressure(event) => Some(event), + PlatformInput::MouseExited(event) => Some(event), + PlatformInput::ScrollWheel(event) => Some(event), + PlatformInput::Pinch(event) => Some(event), + PlatformInput::LongPress(event) => Some(event), + PlatformInput::TouchDrag(event) => Some(event), + PlatformInput::FileDrop(event) => Some(event), + PlatformInput::Touch(_) => None, + } + } + + pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> { + match self { + PlatformInput::KeyDown(event) => Some(event), + PlatformInput::KeyUp(event) => Some(event), + PlatformInput::ModifiersChanged(event) => Some(event), + PlatformInput::MouseDown(_) => None, + PlatformInput::MouseUp(_) => None, + PlatformInput::MouseMove(_) => None, + PlatformInput::MousePressure(_) => None, + PlatformInput::MouseExited(_) => None, + PlatformInput::ScrollWheel(_) => None, + PlatformInput::Pinch(_) => None, + PlatformInput::LongPress(_) => None, + PlatformInput::TouchDrag(_) => None, + PlatformInput::FileDrop(_) => None, + PlatformInput::Touch(_) => None, + } + } + + /// A short static name for this input's variant, for diagnostics and + /// telemetry. + pub fn kind_name(&self) -> &'static str { + match self { + PlatformInput::KeyDown(_) => "key_down", + PlatformInput::KeyUp(_) => "key_up", + PlatformInput::ModifiersChanged(_) => "modifiers_changed", + PlatformInput::MouseDown(_) => "mouse_down", + PlatformInput::MouseUp(_) => "mouse_up", + PlatformInput::MousePressure(_) => "mouse_pressure", + PlatformInput::MouseMove(_) => "mouse_move", + PlatformInput::MouseExited(_) => "mouse_exited", + PlatformInput::ScrollWheel(_) => "scroll_wheel", + PlatformInput::Pinch(_) => "pinch", + PlatformInput::LongPress(_) => "long_press", + PlatformInput::TouchDrag(_) => "touch_drag", + PlatformInput::FileDrop(_) => "file_drop", + PlatformInput::Touch(_) => "touch", + } + } + + /// Returns the touch event contained in this input, if any. + pub fn touch_event(&self) -> Option<&TouchEvent> { + match self { + PlatformInput::Touch(event) => Some(event), + _ => None, + } + } +} + +#[cfg(test)] +mod test { + + use crate::{ + self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, + KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div, + }; + + struct TestView { + saw_key_down: bool, + saw_action: bool, + focus_handle: FocusHandle, + } + + actions!(test_only, [TestAction]); + + impl Render for TestView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div().id("testview").child( + div() + .key_context("parent") + .on_key_down(cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.saw_key_down = true + })) + .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| { + this.saw_action = true + })) + .child( + div() + .key_context("nested") + .track_focus(&self.focus_handle) + .into_element(), + ), + ) + } + } + + #[gpui::test] + fn test_on_events(cx: &mut TestAppContext) { + let window = cx.update(|cx| { + cx.open_window(Default::default(), |_, cx| { + cx.new(|cx| TestView { + saw_key_down: false, + saw_action: false, + focus_handle: cx.focus_handle(), + }) + }) + .unwrap() + }); + + cx.update(|cx| { + cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); + }); + + window + .update(cx, |test_view, window, cx| { + window.focus(&test_view.focus_handle, cx) + }) + .unwrap(); + + cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap()); + cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap()); + + window + .update(cx, |test_view, _, _| { + assert!(test_view.saw_key_down || test_view.saw_action); + assert!(test_view.saw_key_down); + assert!(test_view.saw_action); + }) + .unwrap(); + } + + #[gpui::test] + fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding( + cx: &mut TestAppContext, + ) { + let (test_view, cx) = cx.add_window_view(|_, cx| TestView { + saw_key_down: false, + saw_action: false, + focus_handle: cx.focus_handle(), + }); + + cx.update(|_, cx| { + cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]); + }); + test_view.update_in(cx, |test_view, window, cx| { + window.focus(&test_view.focus_handle, cx); + }); + + cx.simulate_modifiers_change(Modifiers::alt()); + cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::none()); + assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action)); + + cx.simulate_modifiers_change(Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::none()); + assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action)); + } +} diff --git a/vendor/gpui-pre/src/key_dispatch.rs b/vendor/gpui-pre/src/key_dispatch.rs new file mode 100644 index 0000000..2f755b1 --- /dev/null +++ b/vendor/gpui-pre/src/key_dispatch.rs @@ -0,0 +1,2196 @@ +//! KeyDispatch is where GPUI deals with binding actions to key events. +//! +//! The key pieces to making a key binding work are to define an action, +//! implement a method that takes that action as a type parameter, +//! and then to register the action during render on a focused node +//! with a keymap context: +//! +//! ```ignore +//! actions!(editor,[Undo, Redo]); +//! +//! impl Editor { +//! fn undo(&mut self, _: &Undo, _window: &mut Window, _cx: &mut Context) { ... } +//! fn redo(&mut self, _: &Redo, _window: &mut Window, _cx: &mut Context) { ... } +//! } +//! +//! impl Render for Editor { +//! fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { +//! div() +//! .track_focus(&self.focus_handle(cx)) +//! .key_context("Editor") +//! .on_action(cx.listener(Editor::undo)) +//! .on_action(cx.listener(Editor::redo)) +//! ... +//! } +//! } +//!``` +//! +//! The keybindings themselves are managed independently by calling cx.bind_keys(). +//! (Though mostly when developing Zed itself, you just need to add a new line to +//! assets/keymaps/default-{platform}.json). +//! +//! ```ignore +//! cx.bind_keys([ +//! KeyBinding::new("cmd-z", Editor::undo, Some("Editor")), +//! KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")), +//! ]) +//! ``` +//! +//! With all of this in place, GPUI will ensure that if you have an Editor that contains +//! the focus, hitting cmd-z will Undo. +//! +//! In real apps, it is a little more complicated than this, because typically you have +//! several nested views that each register keyboard handlers. In this case action matching +//! bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined +//! then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace. +//! +//! In GPUI, keybindings are not limited to just single keystrokes, you can define +//! sequences by separating the keys with a space: +//! +//! KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) + +use crate::{ + Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + Keystroke, ModifiersChangedEvent, Window, +}; +use collections::FxHashMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + mem, + ops::Range, + rc::Rc, +}; + +/// ID of a node within `DispatchTree`. Note that these are **not** stable between frames, and so a +/// `DispatchNodeId` should only be used with the `DispatchTree` that provided it. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub(crate) struct DispatchNodeId(usize); + +pub(crate) struct DispatchTree { + node_stack: Vec, + pub(crate) context_stack: Vec, + view_stack: Vec, + nodes: Vec, + focusable_node_ids: FxHashMap, + view_node_ids: FxHashMap, + keymap: Rc>, + action_registry: Rc, +} + +#[derive(Default)] +pub(crate) struct DispatchNode { + pub key_listeners: Vec, + pub action_listeners: Vec, + pub modifiers_changed_listeners: Vec, + pub context: Option, + pub focus_id: Option, + view_id: Option, + parent: Option, +} + +pub(crate) struct ReusedSubtree { + old_range: Range, + new_range: Range, + contains_focus: bool, +} + +impl ReusedSubtree { + pub fn refresh_node_id(&self, node_id: DispatchNodeId) -> DispatchNodeId { + debug_assert!( + self.old_range.contains(&node_id.0), + "node {} was not part of the reused subtree {:?}", + node_id.0, + self.old_range + ); + DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) + } + + pub fn contains_focus(&self) -> bool { + self.contains_focus + } +} + +#[derive(Default, Debug)] +pub(crate) struct Replay { + pub(crate) keystroke: Keystroke, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, +} + +#[derive(Default, Debug)] +pub(crate) struct DispatchResult { + pub(crate) pending: SmallVec<[Keystroke; 1]>, + pub(crate) pending_has_binding: bool, + pub(crate) bindings: SmallVec<[KeyBinding; 1]>, + pub(crate) to_replay: SmallVec<[Replay; 1]>, + pub(crate) context_stack: Vec, +} + +type KeyListener = Rc; +type ModifiersChangedListener = Rc; + +#[derive(Clone)] +pub(crate) struct DispatchActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, +} + +impl DispatchTree { + pub fn new(keymap: Rc>, action_registry: Rc) -> Self { + Self { + node_stack: Vec::new(), + context_stack: Vec::new(), + view_stack: Vec::new(), + nodes: Vec::new(), + focusable_node_ids: FxHashMap::default(), + view_node_ids: FxHashMap::default(), + keymap, + action_registry, + } + } + + pub fn clear(&mut self) { + self.node_stack.clear(); + self.context_stack.clear(); + self.view_stack.clear(); + self.nodes.clear(); + self.focusable_node_ids.clear(); + self.view_node_ids.clear(); + } + + pub fn len(&self) -> usize { + self.nodes.len() + } + + pub fn push_node(&mut self) -> DispatchNodeId { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + node_id + } + + pub fn set_active_node(&mut self, node_id: DispatchNodeId) { + let next_node_parent = self.nodes[node_id.0].parent; + while self.node_stack.last().copied() != next_node_parent && !self.node_stack.is_empty() { + self.pop_node(); + } + + if self.node_stack.last().copied() == next_node_parent { + self.node_stack.push(node_id); + let active_node = &self.nodes[node_id.0]; + if let Some(view_id) = active_node.view_id { + self.view_stack.push(view_id) + } + if let Some(context) = active_node.context.clone() { + self.context_stack.push(context); + } + } else { + debug_assert_eq!(self.node_stack.len(), 0); + + let mut current_node_id = Some(node_id); + while let Some(node_id) = current_node_id { + let node = &self.nodes[node_id.0]; + if let Some(context) = node.context.clone() { + self.context_stack.push(context); + } + if let Some(view_id) = node.view_id { + self.view_stack.push(view_id); + } + self.node_stack.push(node_id); + current_node_id = node.parent; + } + + self.context_stack.reverse(); + self.view_stack.reverse(); + self.node_stack.reverse(); + } + } + + pub fn set_key_context(&mut self, context: KeyContext) { + self.active_node().context = Some(context.clone()); + self.context_stack.push(context); + } + + pub fn set_focus_id(&mut self, focus_id: FocusId) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].focus_id = Some(focus_id); + self.focusable_node_ids.insert(focus_id, node_id); + } + + pub fn set_view_id(&mut self, view_id: EntityId) { + if self.view_stack.last().copied() != Some(view_id) { + let node_id = *self.node_stack.last().unwrap(); + self.nodes[node_id.0].view_id = Some(view_id); + self.view_node_ids.insert(view_id, node_id); + self.view_stack.push(view_id); + } + } + + pub fn pop_node(&mut self) { + let node = &self.nodes[self.active_node_id().unwrap().0]; + if node.context.is_some() { + self.context_stack.pop(); + } + if node.view_id.is_some() { + self.view_stack.pop(); + } + self.node_stack.pop(); + } + + fn move_node(&mut self, source: &mut DispatchNode) { + self.push_node(); + if let Some(context) = source.context.clone() { + self.set_key_context(context); + } + if let Some(focus_id) = source.focus_id { + self.set_focus_id(focus_id); + } + if let Some(view_id) = source.view_id { + self.set_view_id(view_id); + } + + let target = self.active_node(); + target.key_listeners = mem::take(&mut source.key_listeners); + target.action_listeners = mem::take(&mut source.action_listeners); + target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners); + } + + pub fn reuse_subtree( + &mut self, + old_range: Range, + source: &mut Self, + focus: Option, + ) -> ReusedSubtree { + let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); + + let mut contains_focus = false; + let mut source_stack = vec![]; + for (source_node_id, source_node) in source + .nodes + .iter_mut() + .enumerate() + .skip(old_range.start) + .take(old_range.len()) + { + let source_node_id = DispatchNodeId(source_node_id); + while let Some(source_ancestor) = source_stack.last() { + if source_node.parent == Some(*source_ancestor) { + break; + } else { + source_stack.pop(); + self.pop_node(); + } + } + + source_stack.push(source_node_id); + if source_node.focus_id.is_some() && source_node.focus_id == focus { + contains_focus = true; + } + self.move_node(source_node); + } + + while !source_stack.is_empty() { + source_stack.pop(); + self.pop_node(); + } + + ReusedSubtree { + old_range, + new_range, + contains_focus, + } + } + + pub fn truncate(&mut self, index: usize) { + for node in &self.nodes[index..] { + if let Some(focus_id) = node.focus_id { + self.focusable_node_ids.remove(&focus_id); + } + + if let Some(view_id) = node.view_id { + self.view_node_ids.remove(&view_id); + } + } + self.nodes.truncate(index); + } + + pub fn on_key_event(&mut self, listener: KeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_modifiers_changed(&mut self, listener: ModifiersChangedListener) { + self.active_node() + .modifiers_changed_listeners + .push(listener); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + listener: Rc, + ) { + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); + } + + pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { + if parent == child { + return true; + } + + if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { + let mut current_node_id = self.focusable_node_ids.get(&child).copied(); + while let Some(node_id) = current_node_id { + if node_id == *parent_node_id { + return true; + } + current_node_id = self.nodes[node_id.0].parent; + } + } + false + } + + pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { + let mut actions = Vec::>::new(); + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) + { + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + let action = self.action_registry.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + } + actions + } + + pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + false + } + + /// Returns key bindings that invoke an action on the currently focused element. Bindings are + /// returned in the order they were added. For display, the last binding should take precedence. + /// + /// Bindings are only included if they are the highest precedence match for their keystrokes, so + /// shadowed bindings are not included. + pub fn bindings_for_action( + &self, + action: &dyn Action, + context_stack: &[KeyContext], + ) -> Vec { + // Ideally this would return a `DoubleEndedIterator` to avoid `highest_precedence_*` + // methods, but this can't be done very cleanly since keymap must be borrowed. + let keymap = self.keymap.borrow(); + keymap + .bindings_for_action(action) + .filter(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) + }) + .cloned() + .collect() + } + + /// Returns the highest precedence binding for the given action and context stack. This is the + /// same as the last result of `bindings_for_action`, but more efficient than getting all bindings. + pub fn highest_precedence_binding_for_action( + &self, + action: &dyn Action, + context_stack: &[KeyContext], + ) -> Option { + let keymap = self.keymap.borrow(); + keymap + .bindings_for_action(action) + .rev() + .find(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack) + }) + .cloned() + } + + fn binding_matches_predicate_and_not_shadowed( + keymap: &Keymap, + binding: &KeyBinding, + context_stack: &[KeyContext], + ) -> bool { + let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack); + if let Some(found) = bindings.iter().next() { + found.action.partial_eq(binding.action.as_ref()) + } else { + false + } + } + + fn bindings_for_input( + &self, + input: &[Keystroke], + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> (SmallVec<[KeyBinding; 1]>, bool, Vec) { + let context_stack: Vec = dispatch_path + .iter() + .filter_map(|node_id| self.node(*node_id).context.clone()) + .collect(); + + let (bindings, partial) = self + .keymap + .borrow() + .bindings_for_input(input, &context_stack); + (bindings, partial, context_stack) + } + + /// Find the bindings that can follow the current input sequence. + pub fn possible_next_bindings_for_input( + &self, + input: &[Keystroke], + context_stack: &[KeyContext], + ) -> Vec { + self.keymap + .borrow() + .possible_next_bindings_for_input(input, context_stack) + } + + /// dispatch_key processes the keystroke + /// input should be set to the value of `pending` from the previous call to dispatch_key. + /// This returns three instructions to the input handler: + /// - bindings: any bindings to execute before processing this keystroke + /// - pending: the new set of pending keystrokes to store + /// - to_replay: any keystroke that had been pushed to pending, but are no-longer matched, + /// these should be replayed first. + pub fn dispatch_key( + &mut self, + mut input: SmallVec<[Keystroke; 1]>, + keystroke: Keystroke, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> DispatchResult { + input.push(keystroke.clone()); + let (bindings, pending, context_stack) = self.bindings_for_input(&input, dispatch_path); + + if pending { + return DispatchResult { + pending: input, + pending_has_binding: !bindings.is_empty(), + context_stack, + ..Default::default() + }; + } else if !bindings.is_empty() { + return DispatchResult { + bindings, + context_stack, + ..Default::default() + }; + } else if input.len() == 1 { + return DispatchResult { + context_stack, + ..Default::default() + }; + } + input.pop(); + + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); + + let mut result = self.dispatch_key(suffix, keystroke, dispatch_path); + to_replay.extend(result.to_replay); + result.to_replay = to_replay; + result + } + + /// If the user types a matching prefix of a binding and then waits for a timeout + /// flush_dispatch() converts any previously pending input to replay events. + pub fn flush_dispatch( + &mut self, + input: SmallVec<[Keystroke; 1]>, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> SmallVec<[Replay; 1]> { + let (suffix, mut to_replay) = self.replay_prefix(input, dispatch_path); + + if !suffix.is_empty() { + to_replay.extend(self.flush_dispatch(suffix, dispatch_path)) + } + + to_replay + } + + /// Converts the longest prefix of input to a replay event and returns the rest. + fn replay_prefix( + &self, + mut input: SmallVec<[Keystroke; 1]>, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + ) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { + let mut to_replay: SmallVec<[Replay; 1]> = Default::default(); + for last in (0..input.len()).rev() { + let (bindings, _, _) = self.bindings_for_input(&input[0..=last], dispatch_path); + if !bindings.is_empty() { + to_replay.push(Replay { + keystroke: input.drain(0..=last).next_back().unwrap(), + bindings, + }); + break; + } + } + if to_replay.is_empty() { + to_replay.push(Replay { + keystroke: input.remove(0), + ..Default::default() + }); + } + (input, to_replay) + } + + pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent); + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } + + pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> { + let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new(); + let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied(); + while let Some(node_id) = current_node_id { + let node = self.node(node_id); + if let Some(focus_id) = node.focus_id { + focus_path.push(focus_id); + } + current_node_id = node.parent; + } + focus_path.reverse(); // Reverse the path so it goes from the root to the focused node. + focus_path + } + + pub fn view_path_reversed(&self, view_id: EntityId) -> impl Iterator { + let mut current_node_id = self.view_node_ids.get(&view_id).copied(); + + std::iter::successors( + current_node_id.map(|node_id| self.node(node_id)), + |node_id| Some(self.node(node_id.parent?)), + ) + .filter_map(|node| node.view_id) + } + + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id().unwrap(); + &mut self.nodes[active_node_id.0] + } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } + + pub fn root_node_id(&self) -> DispatchNodeId { + debug_assert!(!self.nodes.is_empty()); + DispatchNodeId(0) + } + + pub fn active_node_id(&self) -> Option { + self.node_stack.last().copied() + } +} + +#[cfg(test)] +mod tests { + use crate::{ + self as gpui, AppContext, DispatchResult, Element, ElementId, GlobalElementId, + InspectorElementId, Keystroke, LayoutId, Style, + }; + use core::panic; + use smallvec::SmallVec; + use std::{ + cell::{Cell, RefCell}, + ops::Range, + rc::Rc, + }; + + use crate::{ + Action, ActionRegistry, App, Bounds, Context, DispatchEventResult, DispatchPhase, + DispatchTree, Entity, FocusHandle, InputHandler, InputPreference, IntoElement, KeyBinding, + KeyContext, KeyDownEvent, Keymap, Modifiers, Pixels, PlatformInput, PlatformWindow, Point, + Render, Subscription, TestAppContext, UTF16Selection, Unbind, VisualContext, + VisualTestContext, Window, + }; + + actions!(dispatch_test, [TestAction, SecondaryTestAction]); + + fn test_dispatch_tree(bindings: Vec) -> DispatchTree { + let registry = ActionRegistry::default(); + + DispatchTree::new( + Rc::new(RefCell::new(Keymap::new(bindings))), + Rc::new(registry), + ) + } + + struct PendingInputTestView { + focus_handle: FocusHandle, + action_count: Rc>, + secondary_action_count: Rc>, + } + + #[derive(Clone)] + struct PendingTextInputTestView { + focus_handle: FocusHandle, + text: Rc>, + action_count: Rc>, + accepts_text_input: Rc>, + } + + impl PendingTextInputTestView { + fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + text: Rc::default(), + action_count: Rc::default(), + accepts_text_input: Rc::new(Cell::new(true)), + } + } + } + + impl Element for PendingTextInputTestView { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + Some("pending-text-input-test".into()) + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + window.set_focus_handle(&self.focus_handle, cx); + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let mut key_context = KeyContext::default(); + key_context.add("Terminal"); + window.set_key_context(key_context); + window.handle_input(&self.focus_handle, self.clone(), cx); + let action_count = self.action_count.clone(); + window.on_action( + std::any::TypeId::of::(), + move |_, phase, _, _| { + if phase == DispatchPhase::Bubble { + action_count.set(action_count.get() + 1); + } + }, + ); + } + } + + impl IntoElement for PendingTextInputTestView { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl InputHandler for PendingTextInputTestView { + fn accepts_text_input(&mut self, _: &mut Window, _: &mut App) -> bool { + self.accepts_text_input.get() + } + + fn selected_text_range( + &mut self, + _: bool, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { + None + } + + fn text_for_range( + &mut self, + _: Range, + _: &mut Option>, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(text) + } + + fn replace_and_mark_text_in_range( + &mut self, + replacement_range: Option>, + new_text: &str, + _: Option>, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(new_text) + } + + fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} + + fn prefers_ime_for_printable_keys(&mut self, _: &mut Window, _: &mut App) -> bool { + true + } + + fn bounds_for_range( + &mut self, + _: Range, + _: &mut Window, + _: &mut App, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _: Point, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + } + + impl Render for PendingTextInputTestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.clone() + } + } + + struct PendingInputTimeoutPauseOwner; + + impl Render for PendingInputTestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + use crate::{InteractiveElement as _, Styled as _}; + let action_count = self.action_count.clone(); + let secondary_action_count = self.secondary_action_count.clone(); + crate::div() + .key_context("Terminal") + .track_focus(&self.focus_handle) + .size_full() + .on_action(move |_: &TestAction, _, _| { + action_count.set(action_count.get() + 1); + }) + .on_action(move |_: &SecondaryTestAction, _, _| { + secondary_action_count.set(secondary_action_count.get() + 1); + }) + } + } + + fn setup_pending_input_test( + cx: &mut TestAppContext, + bindings: impl IntoIterator, + ) -> (&mut VisualTestContext, Rc>, Rc>) { + cx.update(|cx| cx.bind_keys(bindings)); + + let action_count = Rc::new(Cell::new(0)); + let secondary_action_count = Rc::new(Cell::new(0)); + let (view, cx) = cx.add_window_view(|_, cx| PendingInputTestView { + focus_handle: cx.focus_handle(), + action_count: action_count.clone(), + secondary_action_count: secondary_action_count.clone(), + }); + let focus_handle = cx.update(|_, cx| view.read(cx).focus_handle.clone()); + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + window.activate_window(); + }); + + (cx, action_count, secondary_action_count) + } + + #[derive(Debug, PartialEq)] + struct ObservedKeystroke { + keystroke: Keystroke, + action_name: Option<&'static str>, + context_stack: Vec, + } + + fn capture_observed_keystrokes( + cx: &mut VisualTestContext, + ) -> (Rc>>, Subscription) { + let observed_keystrokes = Rc::new(RefCell::new(Vec::new())); + let subscription = cx.update(|_, cx| { + cx.observe_keystrokes({ + let observed_keystrokes = observed_keystrokes.clone(); + move |event, _, _| { + observed_keystrokes.borrow_mut().push(ObservedKeystroke { + keystroke: event.keystroke.clone(), + action_name: event.action.as_ref().map(|action| action.name()), + context_stack: event.context_stack.clone(), + }); + } + }) + }); + (observed_keystrokes, subscription) + } + + fn simulate_modifier_tap(cx: &mut VisualTestContext, modifiers: Modifiers) { + cx.simulate_modifiers_change(modifiers); + cx.simulate_modifiers_change(Modifiers::none()); + } + + fn terminal_context() -> KeyContext { + KeyContext::parse("Terminal").expect("valid key context") + } + + fn setup_pending_input_timeout_test( + cx: &mut TestAppContext, + ) -> (&mut VisualTestContext, Rc>, Rc>) { + setup_pending_input_test( + cx, + [ + KeyBinding::new("ctrl-b", TestAction, Some("Terminal")), + KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), + KeyBinding::new("ctrl-b h j", TestAction, Some("Terminal")), + ], + ) + } + + fn query_prefers_ime_for_printable_keys(cx: &mut VisualTestContext) -> Option { + let mut platform_window = cx.test_window(cx.window_handle()); + let mut input_handler = platform_window.take_input_handler()?; + let prefers_ime = input_handler.query_prefers_ime_for_printable_keys(); + platform_window.set_input_handler(input_handler); + Some(prefers_ime) + } + + fn setup_altgr_test( + cx: &mut TestAppContext, + ) -> (Entity, &mut VisualTestContext) { + let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); + test.update_in(cx, |test, window, cx| { + window.focus(&test.focus_handle, cx); + window.activate_window(); + cx.bind_keys([KeyBinding::new("ctrl-alt-a", TestAction, Some("Terminal"))]); + }); + (test, cx) + } + + fn dispatch_altgr_key_down( + cx: &mut VisualTestContext, + prefer_character_input: bool, + ) -> DispatchEventResult { + cx.update(|window, cx| { + window.dispatch_event( + PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke { + key_char: Some("ą".into()), + ..Keystroke::parse("ctrl-alt-a").expect("valid keystroke") + }, + is_held: false, + prefer_character_input, + }), + cx, + ) + }) + } + + fn simulate_pending_binding(cx: &mut VisualTestContext) { + cx.simulate_modifiers_change(crate::Modifiers::control()); + cx.simulate_keystrokes("ctrl-b"); + cx.simulate_modifiers_change(crate::Modifiers::default()); + } + + #[test] + fn test_keybinding_for_action_bounds() { + let tree = test_dispatch_tree(vec![KeyBinding::new( + "cmd-n", + TestAction, + Some("ProjectPanel"), + )]); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("ProjectPanel").unwrap(), + ]; + + let keybinding = tree.bindings_for_action(&TestAction, &contexts); + + assert!(keybinding[0].action.partial_eq(&TestAction)) + } + + #[test] + fn test_bindings_for_action_hides_targeted_unbind_in_active_context() { + let tree = test_dispatch_tree(vec![ + KeyBinding::new("tab", TestAction, Some("Editor")), + KeyBinding::new( + "tab", + Unbind("dispatch_test::TestAction".into()), + Some("Editor && edit_prediction"), + ), + KeyBinding::new( + "tab", + SecondaryTestAction, + Some("Editor && showing_completions"), + ), + ]); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("Editor showing_completions edit_prediction").unwrap(), + ]; + + let bindings = tree.bindings_for_action(&TestAction, &contexts); + assert!(bindings.is_empty()); + + let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts); + assert!(highest.is_none()); + + let fallback_bindings = tree.bindings_for_action(&SecondaryTestAction, &contexts); + assert_eq!(fallback_bindings.len(), 1); + assert!(fallback_bindings[0].action.partial_eq(&SecondaryTestAction)); + } + + #[test] + fn test_bindings_for_action_keeps_targeted_binding_outside_unbind_context() { + let tree = test_dispatch_tree(vec![ + KeyBinding::new("tab", TestAction, Some("Editor")), + KeyBinding::new( + "tab", + Unbind("dispatch_test::TestAction".into()), + Some("Editor && edit_prediction"), + ), + KeyBinding::new( + "tab", + SecondaryTestAction, + Some("Editor && showing_completions"), + ), + ]); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("Editor").unwrap(), + ]; + + let bindings = tree.bindings_for_action(&TestAction, &contexts); + assert_eq!(bindings.len(), 1); + assert!(bindings[0].action.partial_eq(&TestAction)); + + let highest = tree.highest_precedence_binding_for_action(&TestAction, &contexts); + assert!(highest.is_some_and(|binding| binding.action.partial_eq(&TestAction))); + } + + /// Models the picker preview footer scenario: a picker action is bound in + /// `Picker > Editor`, but a base keymap binds the same chord to an editor + /// action in `Editor`. `Picker > Editor` and `Editor` resolve at the same + /// context depth, so at equal depth precedence is decided purely by load + /// order (later wins). Because base keymaps load after the default keymap, + /// the picker binding is shadowed unless it is (re)bound by an overlay that + /// loads after the base keymap - which is exactly what + /// `keymaps/specific-overrides*.json` does. + #[test] + fn test_overlay_after_base_restores_shadowed_picker_binding() { + // SecondaryTestAction stands in for the editor/base action (e.g. + // editor::AddSelectionBelow), TestAction for the picker action. + let contexts = vec![ + KeyContext::parse("Picker").unwrap(), + KeyContext::parse("Editor").unwrap(), + ]; + + // Default keymap (picker binding) followed by a base keymap that binds + // the same chord to an editor action: the base binding wins and the + // picker action is shadowed, so its footer tooltip renders no shortcut. + let shadowed = test_dispatch_tree(vec![ + KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), + KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), + ]); + let highest = shadowed.highest_precedence_binding_for_action(&TestAction, &contexts); + assert!( + highest.is_none(), + "picker binding should be shadowed by the base editor binding" + ); + + // Re-binding the picker action in an overlay loaded after the base keymap + // restores it as the resolved binding. + let fixed = test_dispatch_tree(vec![ + KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), + KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), + // overlay loaded last: + KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), + ]); + let highest = fixed.highest_precedence_binding_for_action(&TestAction, &contexts); + assert!( + highest.is_some_and(|binding| binding.action.partial_eq(&TestAction)), + "overlay loaded after base should restore the picker binding" + ); + + // Conversely, putting the override in the default keymap (i.e. before the + // base keymap) does NOT help: the later base binding still wins at equal + // depth. This is why the overlay must be loaded after the base keymap. + let override_before_base = test_dispatch_tree(vec![ + KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), + KeyBinding::new("ctrl-alt-down", TestAction, Some("Picker > Editor")), + KeyBinding::new("ctrl-alt-down", SecondaryTestAction, Some("Editor")), + ]); + let highest = + override_before_base.highest_precedence_binding_for_action(&TestAction, &contexts); + assert!( + highest.is_none(), + "an override loaded before the base binding cannot win the equal-depth tie" + ); + } + + #[test] + fn test_pending_has_binding_state() { + let bindings = vec![ + KeyBinding::new("ctrl-b h", TestAction, None), + KeyBinding::new("space", TestAction, Some("ContextA")), + KeyBinding::new("space f g", TestAction, Some("ContextB")), + ]; + let mut tree = test_dispatch_tree(bindings); + + type DispatchPath = SmallVec<[super::DispatchNodeId; 32]>; + fn dispatch( + tree: &mut DispatchTree, + pending: SmallVec<[Keystroke; 1]>, + key: &str, + path: &DispatchPath, + ) -> DispatchResult { + tree.dispatch_key(pending, Keystroke::parse(key).unwrap(), path) + } + + let dispatch_path: DispatchPath = SmallVec::new(); + let result = dispatch(&mut tree, SmallVec::new(), "ctrl-b", &dispatch_path); + assert_eq!(result.pending.len(), 1); + assert!(!result.pending_has_binding); + + let result = dispatch(&mut tree, result.pending, "h", &dispatch_path); + assert_eq!(result.pending.len(), 0); + assert_eq!(result.bindings.len(), 1); + assert!(!result.pending_has_binding); + + let node_id = tree.push_node(); + tree.set_key_context(KeyContext::parse("ContextB").unwrap()); + tree.pop_node(); + + let dispatch_path = tree.dispatch_path(node_id); + let result = dispatch(&mut tree, SmallVec::new(), "space", &dispatch_path); + + assert_eq!(result.pending.len(), 1); + assert!(!result.pending_has_binding); + } + + #[crate::test] + fn test_pending_input_observers_notified_on_focus_change_and_blur(cx: &mut TestAppContext) { + #[derive(Clone)] + struct CustomElement { + focus_handle: FocusHandle, + text: Rc>, + } + + impl CustomElement { + fn new(cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + text: Rc::default(), + } + } + } + + impl Element for CustomElement { + type RequestLayoutState = (); + + type PrepaintState = (); + + fn id(&self) -> Option { + Some("custom".into()) + } + + fn source_location(&self) -> Option<&'static panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + window.set_focus_handle(&self.focus_handle, cx); + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + let mut key_context = KeyContext::default(); + key_context.add("Terminal"); + window.set_key_context(key_context); + window.handle_input(&self.focus_handle, self.clone(), cx); + window.on_action(std::any::TypeId::of::(), |_, _, _, _| {}); + } + } + + impl IntoElement for CustomElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl InputHandler for CustomElement { + fn selected_text_range( + &mut self, + _: bool, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { + None + } + + fn text_for_range( + &mut self, + _: Range, + _: &mut Option>, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(text) + } + + fn replace_and_mark_text_in_range( + &mut self, + replacement_range: Option>, + new_text: &str, + _: Option>, + _: &mut Window, + _: &mut App, + ) { + if replacement_range.is_some() { + unimplemented!() + } + self.text.borrow_mut().push_str(new_text) + } + + fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} + + fn bounds_for_range( + &mut self, + _: Range, + _: &mut Window, + _: &mut App, + ) -> Option> { + None + } + + fn character_index_for_point( + &mut self, + _: Point, + _: &mut Window, + _: &mut App, + ) -> Option { + None + } + } + + impl Render for CustomElement { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.clone() + } + } + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-d", TestAction, None)]); + cx.bind_keys([KeyBinding::new("ctrl-d h", TestAction, None)]); + }); + + let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx)); + let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); + + let pending_input_changed_count = Rc::new(RefCell::new(0usize)); + let pending_input_changed_count_for_observer = pending_input_changed_count.clone(); + + struct PendingInputObserver { + _subscription: Subscription, + } + + let _observer = cx.update(|window, cx| { + cx.new(|cx| PendingInputObserver { + _subscription: cx.observe_pending_input(window, move |_, _, _| { + *pending_input_changed_count_for_observer.borrow_mut() += 1; + }), + }) + }); + + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + window.activate_window(); + }); + + cx.simulate_keystrokes("ctrl-b"); + + let count_after_pending = Rc::new(RefCell::new(0usize)); + let count_after_pending_for_assertion = count_after_pending.clone(); + + cx.update(|window, cx| { + assert!(window.has_pending_keystrokes()); + *count_after_pending.borrow_mut() = *pending_input_changed_count.borrow(); + assert!(*count_after_pending.borrow() > 0); + + window.focus(&cx.focus_handle(), cx); + + assert!(!window.has_pending_keystrokes()); + }); + + // Focus-triggered pending-input notifications are deferred to the end of the current + // effect cycle, so the observer callback should run after the focus update completes. + cx.update(|_, _| { + let count_after_focus_change = *pending_input_changed_count.borrow(); + assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow()); + }); + + cx.update(|window, cx| window.focus(&focus_handle, cx)); + cx.simulate_keystrokes("ctrl-b"); + let count_before_blur = *pending_input_changed_count.borrow(); + + cx.update(|window, cx| { + assert!(window.has_pending_keystrokes()); + window.blur(cx); + assert!(!window.has_pending_keystrokes()); + assert!(window.pending_input_is_none()); + }); + + cx.update(|_, _| { + assert!(*pending_input_changed_count.borrow() > count_before_blur); + }); + + cx.update(|window, cx| window.disable_focus(cx)); + cx.simulate_keystrokes("ctrl-d"); + + cx.update(|window, cx| { + assert!(window.has_pending_keystrokes()); + window.blur(cx); + assert!(window.pending_input_is_none()); + }); + } + + #[crate::test] + fn test_standalone_modifier_dispatch(cx: &mut TestAppContext) { + let (cx, action_count, _) = + setup_pending_input_test(cx, [KeyBinding::new("shift", TestAction, Some("Terminal"))]); + let intercepted_keystrokes = Rc::new(RefCell::new(Vec::new())); + let should_consume = Rc::new(Cell::new(true)); + let terminal_context = terminal_context(); + let (observed_keystrokes, _observer) = capture_observed_keystrokes(cx); + let _interceptor = cx.update(|_, cx| { + cx.intercept_keystrokes({ + let intercepted_keystrokes = intercepted_keystrokes.clone(); + let should_consume = should_consume.clone(); + let terminal_context = terminal_context.clone(); + move |event, _, cx| { + assert!(event.action.is_none()); + assert_eq!(event.context_stack, vec![terminal_context.clone()]); + intercepted_keystrokes + .borrow_mut() + .push(event.keystroke.clone()); + if should_consume.get() { + cx.stop_propagation(); + } + } + }) + }); + let shift = Keystroke::parse("shift").expect("valid keystroke"); + + // Pressing Shift is not a keystroke. Consuming its recognized release suppresses + // the binding. + cx.simulate_modifiers_change(Modifiers::shift()); + assert!(intercepted_keystrokes.borrow().is_empty()); + cx.simulate_modifiers_change(Modifiers::none()); + assert_eq!( + intercepted_keystrokes.borrow().as_slice(), + std::slice::from_ref(&shift) + ); + assert_eq!(action_count.get(), 0); + assert!(observed_keystrokes.borrow().is_empty()); + + // Without consumption, the same recognized release reaches keymap dispatch. + should_consume.set(false); + intercepted_keystrokes.borrow_mut().clear(); + cx.simulate_modifiers_change(Modifiers::shift()); + assert!(intercepted_keystrokes.borrow().is_empty()); + cx.simulate_modifiers_change(Modifiers::none()); + assert_eq!( + intercepted_keystrokes.borrow().as_slice(), + std::slice::from_ref(&shift) + ); + assert_eq!(action_count.get(), 1); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: shift, + action_name: Some(TestAction.name()), + context_stack: vec![terminal_context], + }] + ); + } + + #[crate::test] + fn test_unbound_standalone_modifier_observation(cx: &mut TestAppContext) { + let (cx, _, _) = setup_pending_input_test(cx, []); + let terminal_context = terminal_context(); + let (observed_keystrokes, _observer) = capture_observed_keystrokes(cx); + + for (modifiers, key) in [ + (Modifiers::shift(), "shift"), + (Modifiers::control(), "ctrl"), + (Modifiers::alt(), "alt"), + (Modifiers::command(), "cmd"), + (Modifiers::function(), "fn"), + ] { + observed_keystrokes.borrow_mut().clear(); + cx.simulate_modifiers_change(modifiers); + assert!( + observed_keystrokes.borrow().is_empty(), + "modifier press must not notify observers" + ); + cx.simulate_modifiers_change(Modifiers::none()); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: Keystroke::parse(key).expect("valid modifier"), + action_name: None, + context_stack: vec![terminal_context.clone()], + }] + ); + } + } + + #[crate::test] + fn test_raw_modifier_handler_can_suppress_observation(cx: &mut TestAppContext) { + struct ModifierListenerTestView { + focus_handle: FocusHandle, + consume: Rc>, + releases: Rc>, + } + + impl Render for ModifierListenerTestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + use crate::{InteractiveElement as _, Styled as _}; + let consume = self.consume.clone(); + let releases = self.releases.clone(); + crate::div() + .key_context("Terminal") + .track_focus(&self.focus_handle) + .size_full() + .on_action(|_: &TestAction, _, cx| cx.propagate()) + .on_modifiers_changed(move |event, _, cx| { + if !event.modifiers.modified() { + releases.set(releases.get() + 1); + if consume.get() { + cx.stop_propagation(); + } + } + }) + } + } + + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("shift", TestAction, Some("Terminal"))]); + }); + let consume = Rc::new(Cell::new(true)); + let releases = Rc::new(Cell::new(0)); + let (view, cx) = cx.add_window_view(|_, cx| ModifierListenerTestView { + focus_handle: cx.focus_handle(), + consume: consume.clone(), + releases: releases.clone(), + }); + cx.update(|window, cx| { + let focus_handle = view.read(cx).focus_handle.clone(); + window.focus(&focus_handle, cx); + window.activate_window(); + }); + let (observed_keystrokes, _observer) = capture_observed_keystrokes(cx); + + simulate_modifier_tap(cx, Modifiers::shift()); + assert_eq!(releases.get(), 1); + assert!(observed_keystrokes.borrow().is_empty()); + + consume.set(false); + simulate_modifier_tap(cx, Modifiers::shift()); + assert_eq!(releases.get(), 2); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: Keystroke::parse("shift").expect("valid modifier"), + action_name: None, + context_stack: vec![terminal_context()], + }] + ); + } + + #[crate::test] + fn test_pending_modifier_observed_when_binding_resolves(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_test( + cx, + [ + KeyBinding::new("shift", TestAction, Some("Terminal")), + KeyBinding::new("shift f1", SecondaryTestAction, Some("Terminal")), + ], + ); + let (observed_keystrokes, _observer) = capture_observed_keystrokes(cx); + + simulate_modifier_tap(cx, Modifiers::shift()); + assert!(observed_keystrokes.borrow().is_empty()); + assert_eq!(action_count.get(), 0); + + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + assert_eq!(action_count.get(), 1); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: Keystroke::parse("shift").expect("valid modifier"), + action_name: Some(TestAction.name()), + context_stack: Vec::new(), + }] + ); + + observed_keystrokes.borrow_mut().clear(); + simulate_modifier_tap(cx, Modifiers::shift()); + assert!(observed_keystrokes.borrow().is_empty()); + cx.simulate_keystrokes("f1"); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 1); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: Keystroke::parse("f1").expect("valid key"), + action_name: Some(SecondaryTestAction.name()), + context_stack: vec![terminal_context()], + }] + ); + } + + #[crate::test] + fn test_combined_shortcut_has_no_standalone_modifier(cx: &mut TestAppContext) { + let (cx, action_count, _) = setup_pending_input_test( + cx, + [ + KeyBinding::new("shift", TestAction, Some("Terminal")), + KeyBinding::new("shift-f1", TestAction, Some("Terminal")), + ], + ); + let intercepted_keystrokes = Rc::new(RefCell::new(Vec::new())); + let (observed_keystrokes, _observer) = capture_observed_keystrokes(cx); + let _interceptor = cx.update(|_, cx| { + cx.intercept_keystrokes({ + let intercepted_keystrokes = intercepted_keystrokes.clone(); + move |event, _, _| { + intercepted_keystrokes + .borrow_mut() + .push(event.keystroke.clone()); + } + }) + }); + + // Shift-F1 is the only keystroke. Releasing Shift must not emit a second one. + cx.simulate_modifiers_change(Modifiers::shift()); + assert!(intercepted_keystrokes.borrow().is_empty()); + cx.simulate_keystrokes("shift-f1"); + cx.simulate_modifiers_change(Modifiers::none()); + + let shift_f1 = Keystroke::parse("shift-f1").expect("valid keystroke"); + assert_eq!( + intercepted_keystrokes.borrow().as_slice(), + std::slice::from_ref(&shift_f1) + ); + assert_eq!( + observed_keystrokes.borrow().as_slice(), + &[ObservedKeystroke { + keystroke: shift_f1, + action_name: Some(TestAction.name()), + context_stack: vec![terminal_context()], + }] + ); + assert_eq!(action_count.get(), 1); + } + + #[crate::test] + fn test_keystroke_input_preference(cx: &mut TestAppContext) { + let (test, cx) = setup_altgr_test(cx); + let intercepted_preferences = Rc::new(RefCell::new(Vec::new())); + let observed_preferences = Rc::new(RefCell::new(Vec::new())); + let _interceptor = cx.update(|_, cx| { + cx.intercept_keystrokes({ + let intercepted_preferences = intercepted_preferences.clone(); + move |event, _, _| { + intercepted_preferences + .borrow_mut() + .push(event.input_preference); + } + }) + }); + let _observer = cx.update(|_, cx| { + cx.observe_keystrokes({ + let observed_preferences = observed_preferences.clone(); + move |event, _, _| { + observed_preferences + .borrow_mut() + .push(event.input_preference); + } + }) + }); + + for (prefer_character_input, accepts_text_input, input_preference) in [ + (true, true, InputPreference::CharacterInput), + (false, true, InputPreference::KeyBindings), + (true, false, InputPreference::KeyBindings), + (false, false, InputPreference::KeyBindings), + ] { + test.update(cx, |test, _| { + test.accepts_text_input.set(accepts_text_input); + test.action_count.set(0); + }); + intercepted_preferences.borrow_mut().clear(); + observed_preferences.borrow_mut().clear(); + let result = dispatch_altgr_key_down(cx, prefer_character_input); + assert_eq!( + result.propagate, + input_preference == InputPreference::CharacterInput + ); + assert_eq!( + intercepted_preferences.borrow().as_slice(), + &[input_preference] + ); + assert_eq!( + observed_preferences.borrow().as_slice(), + &[input_preference] + ); + test.read_with(cx, |test, _| { + assert_eq!( + test.action_count.get(), + usize::from(input_preference == InputPreference::KeyBindings) + ); + }); + } + } + + #[crate::test] + fn test_input_preference_rechecked_after_interception(cx: &mut TestAppContext) { + let (test, cx) = setup_altgr_test(cx); + let accepts_text_input = test.read_with(cx, |test, _| test.accepts_text_input.clone()); + let _interceptor = cx.update(|_, cx| { + cx.intercept_keystrokes({ + let accepts_text_input = accepts_text_input.clone(); + move |event, _, _| { + assert_eq!( + event.input_preference == InputPreference::CharacterInput, + accepts_text_input.get() + ); + accepts_text_input.set(!accepts_text_input.get()); + } + }) + }); + let observed_preferences = Rc::new(RefCell::new(Vec::new())); + let _observer = cx.update(|_, cx| { + cx.observe_keystrokes({ + let observed_preferences = observed_preferences.clone(); + move |event, _, _| { + observed_preferences + .borrow_mut() + .push(event.input_preference); + } + }) + }); + + for (initially_accepts_text_input, observed_preference) in [ + (true, InputPreference::KeyBindings), + (false, InputPreference::CharacterInput), + ] { + accepts_text_input.set(initially_accepts_text_input); + test.update(cx, |test, _| test.action_count.set(0)); + observed_preferences.borrow_mut().clear(); + let result = dispatch_altgr_key_down(cx, true); + assert_eq!(result.propagate, !initially_accepts_text_input); + assert_eq!( + observed_preferences.borrow().as_slice(), + &[observed_preference] + ); + test.read_with(cx, |test, _| { + assert_eq!( + test.action_count.get(), + usize::from(initially_accepts_text_input) + ); + }); + } + } + + #[crate::test] + fn test_keystroke_interceptors_can_consume_preferred_character_input(cx: &mut TestAppContext) { + let (test, cx) = setup_altgr_test(cx); + let intercepted = Rc::new(Cell::new(false)); + let _interceptor = cx.update(|_, cx| { + cx.intercept_keystrokes({ + let intercepted = intercepted.clone(); + move |event, _, cx| { + assert_eq!(event.input_preference, InputPreference::CharacterInput); + intercepted.set(true); + cx.stop_propagation(); + } + }) + }); + + let result = dispatch_altgr_key_down(cx, true); + assert!(intercepted.get()); + assert!(!result.propagate); + test.read_with(cx, |test, _| assert_eq!(test.action_count.get(), 0)); + } + + #[crate::test] + fn test_consumed_modifier_preserves_pending_input(cx: &mut TestAppContext) { + let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); + test.update_in(cx, |test, window, cx| { + window.focus(&test.focus_handle, cx); + window.activate_window(); + }); + let _subscription = cx.update(|_, cx| { + cx.intercept_keystrokes(|event, _, cx| { + if event.keystroke.key == "shift" { + cx.stop_propagation(); + } + }) + }); + + // Exercise both paths that processing Shift would take: completing `j shift`, and + // replaying `j` when `j k` no longer matches. + for binding in ["j shift", "j k"] { + cx.update(|_, cx| { + cx.clear_key_bindings(); + cx.bind_keys([KeyBinding::new(binding, TestAction, Some("Terminal"))]); + }); + test.update(cx, |test, _| test.text.borrow_mut().clear()); + cx.simulate_keystrokes("j"); + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT / 2); + cx.run_until_parked(); + let pending_keystrokes = cx.update(|window, _| { + window + .pending_input() + .expect("pending input") + .keystrokes() + .to_vec() + }); + + // Consuming Shift must not complete or replay the pending `j`. + cx.simulate_modifiers_change(Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::none()); + cx.update(|window, _| { + let pending_input = window.pending_input().expect("pending input"); + assert_eq!(pending_input.keystrokes(), pending_keystrokes); + assert!(pending_input.timeout().is_some()); + }); + test.update(cx, |test, _| { + assert_eq!(test.action_count.get(), 0); + assert_eq!(test.text.borrow().as_str(), ""); + }); + + // The unchanged timeout should replay `j` after its remaining half. + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT / 2); + cx.run_until_parked(); + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + test.update(cx, |test, _| { + assert_eq!(test.action_count.get(), 0); + assert_eq!(test.text.borrow().as_str(), "j"); + }); + } + } + + #[crate::test] + fn test_printable_pending_input_replays_on_timeout(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("j k", TestAction, Some("Terminal"))]); + }); + let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); + let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + window.activate_window(); + }); + + cx.simulate_keystrokes("j"); + + cx.update(|window, _| { + let pending_input = window.pending_input().expect("pending input"); + assert_eq!(pending_input.keystrokes().len(), 1); + assert!(pending_input.timeout().is_some()); + }); + test.update(cx, |test, _| { + assert_eq!(test.action_count.get(), 0); + assert_eq!(test.text.borrow().as_str(), ""); + }); + + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + test.update(cx, |test, _| { + assert_eq!(test.action_count.get(), 0); + assert_eq!(test.text.borrow().as_str(), "j"); + }); + } + + #[crate::test] + fn test_pending_input_timeout_dispatches_shorter_binding(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + cx.update(|window, _| { + assert_eq!( + window + .pending_input() + .map(|pending_input| pending_input.keystrokes().len()), + Some(1) + ); + assert_eq!( + window + .pending_input() + .and_then(|pending_input| pending_input.timeout()) + .map(|timeout| timeout.duration()), + Some(crate::PENDING_INPUT_TIMEOUT) + ); + }); + assert_eq!(action_count.get(), 0); + + // Emulate a countdown indicator re-rendering the window while waiting for the timeout. + for _ in 0..10 { + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT / 10); + cx.update(|window, _| window.refresh()); + cx.run_until_parked(); + } + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 0); + } + + #[crate::test] + fn test_running_pending_input_timeout_resets_when_binding_advances(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 4 / 5); + cx.run_until_parked(); + cx.simulate_keystrokes("h"); + cx.run_until_parked(); + + cx.update(|window, cx| { + let pending_input = window.pending_input().expect("pending input"); + let timeout = pending_input.timeout().expect("pending input timeout"); + assert_eq!(pending_input.keystrokes().len(), 2); + assert!(!timeout.is_paused()); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); + }); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT / 5); + cx.run_until_parked(); + cx.update(|window, _| assert!(window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 0); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 4 / 5); + cx.run_until_parked(); + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 1); + } + + #[crate::test] + fn test_pending_input_timeout_starts_when_binding_becomes_ambiguous(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_test( + cx, + [ + KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), + KeyBinding::new("ctrl-b h j", TestAction, Some("Terminal")), + ], + ); + simulate_pending_binding(cx); + + cx.update(|window, _| { + let pending_input = window.pending_input().expect("pending input"); + assert_eq!(pending_input.keystrokes().len(), 1); + assert!(pending_input.timeout().is_none()); + }); + + cx.simulate_keystrokes("h"); + cx.run_until_parked(); + cx.update(|window, cx| { + let pending_input = window.pending_input().expect("pending input"); + let timeout = pending_input.timeout().expect("pending input timeout"); + assert_eq!(pending_input.keystrokes().len(), 2); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); + }); + + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 1); + } + + #[crate::test] + fn test_invalid_continuation_while_timeout_paused_replays_pending_input( + cx: &mut TestAppContext, + ) { + let (cx, action_count, secondary_action_count) = setup_pending_input_test( + cx, + [ + KeyBinding::new("ctrl-b", TestAction, Some("Terminal")), + KeyBinding::new("ctrl-b h", SecondaryTestAction, Some("Terminal")), + KeyBinding::new("x", SecondaryTestAction, Some("Terminal")), + ], + ); + simulate_pending_binding(cx); + let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + }); + cx.run_until_parked(); + + cx.simulate_keystrokes("x"); + cx.run_until_parked(); + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 1); + + drop(pause_owner); + cx.update(|_, _| {}); + cx.run_until_parked(); + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + + cx.update(|window, _| assert!(window.pending_input_is_none())); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 1); + } + + #[crate::test] + fn test_pending_input_timeout_pauses_and_resumes(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 7 / 10); + cx.run_until_parked(); + + let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + let other_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + assert!(!window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + assert!(!window.set_pending_input_timeout_paused(&other_owner, false, cx)); + }); + cx.run_until_parked(); + cx.update(|window, cx| { + let timeout = window + .pending_input() + .and_then(|pending_input| pending_input.timeout()) + .expect("pending input timeout"); + assert!(timeout.is_paused()); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); + }); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 2); + cx.run_until_parked(); + cx.update(|window, _| assert!(window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, false, cx)); + }); + cx.run_until_parked(); + cx.update(|window, cx| { + let timeout = window + .pending_input() + .and_then(|pending_input| pending_input.timeout()) + .expect("pending input timeout"); + assert!(!timeout.is_paused()); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); + }); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 3 / 10); + cx.run_until_parked(); + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 0); + } + + #[crate::test] + fn test_pending_input_timeout_resumes_when_owner_is_released(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 7 / 10); + cx.run_until_parked(); + + let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + }); + cx.run_until_parked(); + + drop(pause_owner); + cx.update(|_, _| {}); + cx.run_until_parked(); + cx.update(|window, cx| { + let timeout = window + .pending_input() + .and_then(|pending_input| pending_input.timeout()) + .expect("pending input timeout"); + assert!(!timeout.is_paused()); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT * 3 / 10); + }); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 3 / 10); + cx.run_until_parked(); + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 1); + assert_eq!(secondary_action_count.get(), 0); + } + + #[crate::test] + fn test_pending_input_timeout_resets_when_binding_advances(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT / 2); + cx.run_until_parked(); + let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + }); + cx.run_until_parked(); + cx.update(|window, cx| { + let timeout = window + .pending_input() + .and_then(|pending_input| pending_input.timeout()) + .expect("pending input timeout"); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT / 2); + }); + + cx.simulate_keystrokes("h"); + cx.run_until_parked(); + cx.update(|window, cx| { + let pending_input = window.pending_input().expect("pending input"); + let timeout = pending_input.timeout().expect("pending input timeout"); + assert_eq!(pending_input.keystrokes().len(), 2); + assert!(timeout.is_paused()); + assert_eq!(timeout.remaining(cx), crate::PENDING_INPUT_TIMEOUT); + }); + + cx.executor() + .advance_clock(crate::PENDING_INPUT_TIMEOUT * 2); + cx.run_until_parked(); + cx.update(|window, _| assert!(window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 0); + + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, false, cx)); + }); + cx.run_until_parked(); + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 1); + } + + #[crate::test] + fn test_clearing_pending_input_invalidates_timeout_pause(cx: &mut TestAppContext) { + let (cx, action_count, secondary_action_count) = setup_pending_input_timeout_test(cx); + simulate_pending_binding(cx); + let pause_owner = cx.update(|_, cx| cx.new(|_| PendingInputTimeoutPauseOwner)); + cx.update(|window, cx| { + assert!(window.set_pending_input_timeout_paused(&pause_owner, true, cx)); + window.focus(&cx.focus_handle(), cx); + assert!(!window.has_pending_keystrokes()); + }); + + drop(pause_owner); + cx.update(|_, _| {}); + cx.run_until_parked(); + cx.executor().advance_clock(crate::PENDING_INPUT_TIMEOUT); + cx.run_until_parked(); + + cx.update(|window, _| assert!(window.pending_input_is_none())); + assert_eq!(action_count.get(), 0); + assert_eq!(secondary_action_count.get(), 0); + } + + #[crate::test] + fn test_input_handler_pending(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); + cx.bind_keys([KeyBinding::new("ctrl-x k", TestAction, Some("Terminal"))]); + }); + let (test, cx) = cx.add_window_view(|_, cx| PendingTextInputTestView::new(cx)); + let focus_handle = test.update(cx, |test, _| test.focus_handle.clone()); + cx.update(|window, cx| { + window.focus(&focus_handle, cx); + window.activate_window(); + }); + + assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(true)); + cx.simulate_keystrokes("ctrl-x"); + cx.update(|window, _| assert!(window.has_pending_keystrokes())); + assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(false)); + + let prefers_ime_after_blur = { + let mut platform_window = cx.test_window(cx.window_handle()); + let mut input_handler = platform_window.take_input_handler(); + cx.update(|window, cx| { + window.blur(cx); + assert!(!window.has_pending_keystrokes()); + assert!(window.pending_input_is_none()); + }); + let prefers_ime = input_handler + .as_mut() + .map(|input_handler| input_handler.query_prefers_ime_for_printable_keys()); + if let Some(input_handler) = input_handler { + platform_window.set_input_handler(input_handler); + } + prefers_ime + }; + assert_eq!(prefers_ime_after_blur, Some(true)); + cx.update(|window, cx| window.focus(&focus_handle, cx)); + + cx.simulate_keystrokes("ctrl-x"); + assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(false)); + cx.simulate_keystrokes("k"); + cx.update(|window, _| assert!(!window.has_pending_keystrokes())); + assert_eq!(query_prefers_ime_for_printable_keys(cx), Some(true)); + test.update(cx, |test, _| { + assert_eq!(test.action_count.get(), 1); + assert_eq!(test.text.borrow().as_str(), ""); + }); + + cx.simulate_keystrokes("ctrl-b ["); + test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) + } +} diff --git a/vendor/gpui-pre/src/keymap.rs b/vendor/gpui-pre/src/keymap.rs new file mode 100644 index 0000000..477c5a0 --- /dev/null +++ b/vendor/gpui-pre/src/keymap.rs @@ -0,0 +1,1135 @@ +mod binding; +mod context; + +pub use binding::*; +pub use context::*; + +use crate::{Action, AsKeystroke, Keystroke, Unbind, is_no_action, is_unbind}; +use collections::{HashSet, TypeIdHashMap}; +use smallvec::SmallVec; + +/// An opaque identifier of which version of the keymap is currently active. +/// The keymap's version is changed whenever bindings are added or removed. +#[derive(Copy, Clone, Eq, PartialEq, Default)] +pub struct KeymapVersion(usize); + +/// A collection of key bindings for the user's application. +#[derive(Default)] +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_id: TypeIdHashMap>, + disabled_binding_indices: Vec, + version: KeymapVersion, +} + +/// Index of a binding within a keymap. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct BindingIndex(usize); + +fn disabled_binding_matches_context(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool { + match ( + &disabled_binding.context_predicate, + &binding.context_predicate, + ) { + (None, _) => true, + (Some(_), None) => false, + (Some(disabled_predicate), Some(predicate)) => disabled_predicate.is_superset(predicate), + } +} + +fn binding_is_unbound(disabled_binding: &KeyBinding, binding: &KeyBinding) -> bool { + disabled_binding.keystrokes == binding.keystrokes + && disabled_binding + .action() + .as_any() + .downcast_ref::() + .is_some_and(|unbind| unbind.0.as_ref() == binding.action.name()) +} + +impl Keymap { + /// Create a new keymap with the given bindings. + pub fn new(bindings: Vec) -> Self { + let mut this = Self::default(); + this.add_bindings(bindings); + this + } + + /// Get the current version of the keymap. + pub fn version(&self) -> KeymapVersion { + self.version + } + + /// Add more bindings to the keymap. + pub fn add_bindings>(&mut self, bindings: T) { + for binding in bindings { + let action_id = binding.action().as_any().type_id(); + if is_no_action(&*binding.action) || is_unbind(&*binding.action) { + self.disabled_binding_indices.push(self.bindings.len()); + } else { + self.binding_indices_by_action_id + .entry(action_id) + .or_default() + .push(self.bindings.len()); + } + self.bindings.push(binding); + } + + self.version.0 += 1; + } + + /// Reset this keymap to its initial state. + pub fn clear(&mut self) { + self.bindings.clear(); + self.binding_indices_by_action_id.clear(); + self.disabled_binding_indices.clear(); + self.version.0 += 1; + } + + /// Iterate over all bindings, in the order they were added. + pub fn bindings(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + self.bindings.iter() + } + + /// Iterate over all bindings for the given action, in the order they were added. For display, + /// the last binding should take precedence. + pub fn bindings_for_action<'a>( + &'a self, + action: &'a dyn Action, + ) -> impl 'a + DoubleEndedIterator { + let action_id = action.type_id(); + let binding_indices = self + .binding_indices_by_action_id + .get(&action_id) + .map_or(&[] as _, SmallVec::as_slice) + .iter(); + + binding_indices.filter_map(|ix| { + let binding = &self.bindings[*ix]; + if !binding.action().partial_eq(action) { + return None; + } + + for disabled_ix in &self.disabled_binding_indices { + if disabled_ix > ix { + let disabled_binding = &self.bindings[*disabled_ix]; + if disabled_binding.keystrokes != binding.keystrokes { + continue; + } + + if is_no_action(&*disabled_binding.action) { + if disabled_binding_matches_context(disabled_binding, binding) { + return None; + } + } else if is_unbind(&*disabled_binding.action) + && disabled_binding_matches_context(disabled_binding, binding) + && binding_is_unbound(disabled_binding, binding) + { + return None; + } + } + } + + Some(binding) + }) + } + + /// Returns all bindings that might match the input without checking context. The bindings + /// returned in precedence order (reverse of the order they were added to the keymap). + pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { + self.bindings() + .rev() + .filter(|binding| { + binding + .match_keystrokes(input) + .is_some_and(|pending| !pending) + }) + .cloned() + .collect() + } + + /// Returns a list of bindings that match the given input, and a boolean indicating whether or + /// not more bindings might match if the input was longer. Bindings are returned in precedence + /// order (higher precedence first, reverse of the order they were added to the keymap). + /// + /// Precedence is defined by the depth in the tree (matches on the Editor take precedence over + /// matches on the Pane, then the Workspace, etc.). Bindings with no context are treated as the + /// same as the deepest context. + /// + /// In the case of multiple bindings at the same depth, the ones added to the keymap later take + /// precedence. User bindings are added after built-in bindings so that they take precedence. + /// + /// If a binding has been disabled with `"x": null` it will not be returned. Disabled bindings + /// are evaluated with the same precedence rules so you can disable a rule in a given context + /// only. A disabled binding only suppresses bindings from sources with equal or weaker + /// precedence: a base keymap null hides default bindings, but user bindings still apply. + pub fn bindings_for_input( + &self, + input: &[impl AsKeystroke], + context_stack: &[KeyContext], + ) -> (SmallVec<[KeyBinding; 1]>, bool) { + let (bindings, pending) = self.resolve_bindings_for_input( + input, + context_stack, + self.bindings().enumerate().rev(), + ); + ( + bindings + .into_iter() + .map(|(_, binding)| binding.clone()) + .collect(), + pending, + ) + } + + /// Same as `bindings_for_input`, but only looks at `keymap_bindings` instead of the whole + /// keymap, and returns each binding with its keymap index. + /// + /// `keymap_bindings` must be in reverse keymap order and include every binding that could + /// match `input`. For example, leaving out a `null` binding would make the binding it disables + /// look like it wins. `bindings_for_input` passes the whole keymap, which always meets this but + /// goes through every binding on every call. Passing a pre-filtered list is cheaper when + /// resolving many inputs against it. + fn resolve_bindings_for_input<'a>( + &self, + input: &[impl AsKeystroke], + context_stack: &[KeyContext], + keymap_bindings: impl IntoIterator, + ) -> (SmallVec<[(BindingIndex, &'a KeyBinding); 1]>, bool) { + let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); + let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new(); + + for (ix, binding) in keymap_bindings { + let Some(depth) = self.binding_enabled(binding, context_stack) else { + continue; + }; + let Some(pending) = binding.match_keystrokes(input) else { + continue; + }; + + if !pending { + matched_bindings.push((depth, BindingIndex(ix), binding)); + } else { + pending_bindings.push((BindingIndex(ix), binding)); + } + } + + matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); + + let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); + let mut first_binding_index = None; + let mut unbound_bindings: Vec<&KeyBinding> = Vec::new(); + // A `NoAction` binding suppresses out-ranked bindings from sources with + // equal or weaker precedence, while bindings from stronger sources (a + // smaller meta, e.g. a user binding vs a base keymap null) still apply. + // Bindings without a meta are treated as user bindings. + let mut no_action_meta: Option = None; + + for (_, ix, binding) in matched_bindings { + let meta = binding.meta.map_or(0, |meta| meta.0); + if is_no_action(&*binding.action) { + no_action_meta = Some(no_action_meta.map_or(meta, |existing| existing.min(meta))); + continue; + } + + if no_action_meta.is_some_and(|no_action_meta| meta >= no_action_meta) { + continue; + } + + if is_unbind(&*binding.action) { + unbound_bindings.push(binding); + continue; + } + + if unbound_bindings + .iter() + .any(|disabled_binding| binding_is_unbound(disabled_binding, binding)) + { + continue; + } + + bindings.push((ix, binding)); + first_binding_index.get_or_insert(ix); + } + + let mut pending = HashSet::default(); + for (ix, binding) in pending_bindings.into_iter().rev() { + if let Some(binding_ix) = first_binding_index + && binding_ix > ix + { + continue; + } + if is_no_action(&*binding.action) || is_unbind(&*binding.action) { + pending.remove(&&binding.keystrokes); + continue; + } + pending.insert(&binding.keystrokes); + } + + (bindings, !pending.is_empty()) + } + /// Check if the given binding is enabled, given a certain key context. + /// Returns the deepest depth at which the binding matches, or None if it doesn't match. + fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { + if let Some(predicate) = &binding.context_predicate { + predicate.depth_of(contexts) + } else { + Some(contexts.len()) + } + } + + /// Find the bindings that can follow the current input sequence, in precedence order. Only + /// includes bindings that dispatch would run for their full keystrokes, so bindings disabled + /// with `null` or `Unbind` are left out. + pub fn possible_next_bindings_for_input( + &self, + input: &[Keystroke], + context_stack: &[KeyContext], + ) -> Vec { + // Keeps `null` and `Unbind` bindings, which can disable candidates. + let bindings_extending_input = self + .bindings() + .enumerate() + .rev() + .filter(|(_, binding)| { + binding + .match_keystrokes(input) + .is_some_and(|pending| pending) + }) + .collect::>(); + + let mut full_input = SmallVec::<[&Keystroke; 4]>::new(); + let mut candidates = bindings_extending_input + .iter() + .filter(|(_, binding)| !is_no_action(&*binding.action) && !is_unbind(&*binding.action)) + .filter_map(|&(ix, binding)| { + let depth = self.binding_enabled(binding, context_stack)?; + // Use the typed input rather than the binding's own keystrokes. A typed keystroke + // can match through its physical key, like `alt-s`, or the character it produced, + // like `ß`. + full_input.clear(); + full_input.extend(input.iter()); + full_input.extend( + binding.keystrokes[input.len()..] + .iter() + .map(AsKeystroke::as_keystroke), + ); + let (dispatched, _) = self.resolve_bindings_for_input( + &full_input, + context_stack, + bindings_extending_input.iter().copied(), + ); + dispatched + .iter() + .any(|(dispatched_ix, _)| *dispatched_ix == BindingIndex(ix)) + .then_some((depth, ix, binding)) + }) + .collect::>(); + candidates.sort_unstable_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); + + candidates + .into_iter() + .map(|(_, _, binding)| binding.clone()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use gpui::{NoAction, Unbind}; + + actions!( + test_only, + [ActionAlpha, ActionBeta, ActionGamma, ActionDelta,] + ); + + #[test] + fn test_keymap() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, None), + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor && mode==full")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // global bindings are enabled in all contexts + assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0)); + assert_eq!( + keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]), + Some(1) + ); + + // contextual bindings are enabled in contexts that match their predicate + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]), + Some(1) + ); + + assert_eq!( + keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + ), + Some(1) + ); + } + + #[test] + fn test_depth_precedence() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[ + KeyContext::parse("pane").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + + assert!(!pending); + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionGamma {})); + assert!(result[1].action.partial_eq(&ActionBeta {})); + } + + #[test] + fn test_keymap_disabled() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-b", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-a", NoAction {}, Some("editor && mode==full")), + KeyBinding::new("ctrl-b", NoAction {}, None), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // binding is only enabled in a specific context + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty() + ); + assert!( + !keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ) + .0 + .is_empty() + ); + + // binding is disabled in a more specific context + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[KeyContext::parse("editor mode=full").unwrap()], + ) + .0 + .is_empty() + ); + + // binding is globally disabled + assert!( + keymap + .bindings_for_input( + &[Keystroke::parse("ctrl-b").unwrap()], + &[KeyContext::parse("barf").unwrap()], + ) + .0 + .is_empty() + ); + } + + #[test] + /// Tests for https://github.com/zed-industries/zed/issues/30259 + fn test_multiple_keystroke_binding_disabled() { + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space = || Keystroke::parse("space").unwrap(); + let w = || Keystroke::parse("w").unwrap(); + + let space_w = [space(), w()]; + let space_w_w = [space(), w(), w()]; + + let workspace_context = || [KeyContext::parse("workspace").unwrap()]; + + let editor_workspace_context = || { + [ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ] + }; + + // Ensure `space` results in pending input on the workspace, but not editor + let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context()); + assert!(space_workspace.0.is_empty()); + assert!(space_workspace.1); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(!space_editor.1); + + // Ensure `space w` results in pending input on the workspace, but not editor + let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context()); + assert!(space_w_workspace.0.is_empty()); + assert!(space_w_workspace.1); + + let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); + assert!(space_w_editor.0.is_empty()); + assert!(!space_w_editor.1); + + // Ensure `space w w` results in the binding in the workspace, but not in the editor + let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context()); + assert!(!space_w_w_workspace.0.is_empty()); + assert!(!space_w_w_workspace.1); + + let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); + assert!(space_w_w_editor.0.is_empty()); + assert!(!space_w_w_editor.1); + + // Now test what happens if we have another binding defined AFTER the NoAction + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + + // Now test what happens if we have another binding defined BEFORE the NoAction + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + + // Now test what happens if we have another binding defined at a higher context + // that should result in pending + let bindings = [ + KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")), + KeyBinding::new("space w w", NoAction {}, Some("editor")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); + assert!(space_editor.0.is_empty()); + assert!(space_editor.1); + } + + #[test] + fn test_override_multikey() { + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert!(pending); + + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert_eq!(result.len(), 1); + assert!(!pending); + } + + #[test] + fn test_simple_disable() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert!(!pending); + } + + #[test] + fn test_disable_weaker_sources_only() { + const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0); + const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(1); + const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2); + const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3); + + let editor_context = || [KeyContext::parse("editor").unwrap()]; + let ctrl_x = || [Keystroke::parse("ctrl-x").unwrap()]; + + // A base keymap null disables a default binding in the same context. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert!(result.is_empty()); + + // A user binding is not affected by base keymap or default nulls. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionBeta {}, None).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionBeta {})); + + // A user binding at a shallower context is not disabled by a deeper + // base keymap null. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionBeta {}, Some("workspace")).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input( + &ctrl_x(), + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionBeta {})); + + // A vim binding survives a base keymap null, and a user null disables + // everything. + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(BASE), + KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionGamma {})); + + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-x", ActionGamma {}, Some("editor")).with_meta(VIM), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")).with_meta(USER), + ]); + let (result, _) = keymap.bindings_for_input(&ctrl_x(), &editor_context()); + assert!(result.is_empty()); + } + + #[test] + fn test_fail_to_disable() { + // disabled at the wrong level + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 1); + assert!(!pending); + } + + #[test] + fn test_disable_deeper() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 0); + assert!(!pending); + } + + #[test] + fn test_pending_match_enabled() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(matched.1); + } + + #[test] + fn test_pending_match_enabled_extended() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")), + KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + + #[test] + fn test_overriding_prefix() { + let bindings = [ + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + + #[test] + fn test_context_precedence_with_same_source() { + // Test case: User has both Workspace and Editor bindings for the same key + // Editor binding should take precedence over Workspace binding + let bindings = [ + KeyBinding::new("cmd-r", ActionAlpha {}, Some("Workspace")), + KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + // Test with context stack: [Workspace, Editor] (Editor is deeper) + let (result, _) = keymap.bindings_for_input( + &[Keystroke::parse("cmd-r").unwrap()], + &[ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("Editor").unwrap(), + ], + ); + + // Both bindings should be returned, but Editor binding should be first (highest precedence) + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionBeta {})); // Editor binding first + assert!(result[1].action.partial_eq(&ActionAlpha {})); // Workspace binding second + } + + #[test] + fn test_bindings_for_action() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionAlpha {}, Some("pane")), + KeyBinding::new("ctrl-b", ActionBeta {}, Some("editor && mode == full")), + KeyBinding::new("ctrl-c", ActionGamma {}, Some("workspace")), + KeyBinding::new("ctrl-a", NoAction {}, Some("pane && active")), + KeyBinding::new("ctrl-b", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); + assert_bindings(&keymap, &ActionBeta {}, &[]); + assert_bindings(&keymap, &ActionGamma {}, &["ctrl-c"]); + + #[track_caller] + fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { + let actual = keymap + .bindings_for_action(action) + .map(|binding| binding.keystrokes[0].inner().unparse()) + .collect::>(); + assert_eq!(actual, expected, "{:?}", action); + } + } + + #[test] + fn test_targeted_unbind_ignores_target_context() { + let bindings = [ + KeyBinding::new("tab", ActionAlpha {}, Some("Editor")), + KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")), + KeyBinding::new( + "tab", + Unbind("test_only::ActionAlpha".into()), + Some("Editor && edit_prediction"), + ), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("tab").unwrap()], + &[KeyContext::parse("Editor showing_completions edit_prediction").unwrap()], + ); + + assert!(!pending); + assert_eq!(result.len(), 1); + assert!(result[0].action.partial_eq(&ActionBeta {})); + } + + #[test] + fn test_bindings_for_action_keeps_binding_for_narrower_targeted_unbind() { + let bindings = [ + KeyBinding::new("tab", ActionAlpha {}, Some("Editor")), + KeyBinding::new( + "tab", + Unbind("test_only::ActionAlpha".into()), + Some("Editor && edit_prediction"), + ), + KeyBinding::new("tab", ActionBeta {}, Some("Editor && showing_completions")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + assert_bindings(&keymap, &ActionAlpha {}, &["tab"]); + assert_bindings(&keymap, &ActionBeta {}, &["tab"]); + + #[track_caller] + fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { + let actual = keymap + .bindings_for_action(action) + .map(|binding| binding.keystrokes[0].inner().unparse()) + .collect::>(); + assert_eq!(actual, expected, "{:?}", action); + } + } + + #[test] + fn test_bindings_for_action_removes_binding_for_broader_targeted_unbind() { + let bindings = [ + KeyBinding::new("tab", ActionAlpha {}, Some("Editor && edit_prediction")), + KeyBinding::new( + "tab", + Unbind("test_only::ActionAlpha".into()), + Some("Editor"), + ), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings); + + assert!(keymap.bindings_for_action(&ActionAlpha {}).next().is_none()); + } + + #[test] + fn test_source_precedence_sorting() { + // KeybindSource precedence: User (0) > Vim (1) > Base (2) > Default (3) + // Test that user keymaps take precedence over default keymaps at the same context depth + let mut keymap = Keymap::default(); + + // Add a default keymap binding first + let mut default_binding = KeyBinding::new("cmd-r", ActionAlpha {}, Some("Editor")); + default_binding.set_meta(KeyBindingMetaIndex(3)); // Default source + keymap.add_bindings([default_binding]); + + // Add a user keymap binding + let mut user_binding = KeyBinding::new("cmd-r", ActionBeta {}, Some("Editor")); + user_binding.set_meta(KeyBindingMetaIndex(0)); // User source + keymap.add_bindings([user_binding]); + + // Test with Editor context stack + let (result, _) = keymap.bindings_for_input( + &[Keystroke::parse("cmd-r").unwrap()], + &[KeyContext::parse("Editor").unwrap()], + ); + + // User binding should take precedence over default binding + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionBeta {})); + assert!(result[1].action.partial_eq(&ActionAlpha {})); + } + + #[test] + fn test_possible_next_bindings_exclude_null_bindings() { + let keymap = Keymap::new(vec![ + KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")), + KeyBinding::new("ctrl-k b", ActionBeta {}, Some("Editor")), + KeyBinding::new("ctrl-k a", NoAction {}, Some("Editor")), + ]); + + let bindings = keymap.possible_next_bindings_for_input( + &[Keystroke::parse("ctrl-k").unwrap()], + &[KeyContext::parse("Editor").unwrap()], + ); + assert_eq!( + describe_bindings(&bindings), + vec![("ctrl-k b".to_string(), "test_only::ActionBeta")] + ); + } + + #[test] + fn test_possible_next_bindings_exclude_unbound_bindings() { + let keymap = Keymap::new(vec![ + KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")), + KeyBinding::new("ctrl-k b", ActionBeta {}, Some("Editor")), + KeyBinding::new( + "ctrl-k a", + Unbind("test_only::ActionAlpha".into()), + Some("Editor"), + ), + ]); + + let bindings = keymap.possible_next_bindings_for_input( + &[Keystroke::parse("ctrl-k").unwrap()], + &[KeyContext::parse("Editor").unwrap()], + ); + assert_eq!( + describe_bindings(&bindings), + vec![("ctrl-k b".to_string(), "test_only::ActionBeta")] + ); + } + + #[test] + fn test_possible_next_bindings_keep_user_binding_over_base_keymap_null() { + const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(0); + const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(2); + const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(3); + + let mut keymap = Keymap::default(); + keymap.add_bindings([ + KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Editor")).with_meta(DEFAULT), + KeyBinding::new("ctrl-k a", NoAction {}, Some("Editor")).with_meta(BASE), + KeyBinding::new("ctrl-k a", ActionBeta {}, Some("Editor")).with_meta(USER), + ]); + + let bindings = keymap.possible_next_bindings_for_input( + &[Keystroke::parse("ctrl-k").unwrap()], + &[KeyContext::parse("Editor").unwrap()], + ); + assert_eq!( + describe_bindings(&bindings), + vec![("ctrl-k a".to_string(), "test_only::ActionBeta")] + ); + } + + #[test] + fn test_possible_next_bindings_apply_disabling_by_context() { + let keymap = Keymap::new(vec![ + KeyBinding::new("ctrl-k a", ActionAlpha {}, Some("Workspace")), + KeyBinding::new("ctrl-k a", ActionBeta {}, Some("Editor")), + KeyBinding::new("ctrl-k b", ActionGamma {}, Some("Editor")), + // Only removes the editor binding, so the workspace binding still dispatches. + KeyBinding::new( + "ctrl-k a", + Unbind("test_only::ActionBeta".into()), + Some("Editor"), + ), + // Doesn't apply because `Terminal` isn't in the context stack. + KeyBinding::new("ctrl-k b", NoAction {}, Some("Terminal")), + ]); + + let bindings = keymap.possible_next_bindings_for_input( + &[Keystroke::parse("ctrl-k").unwrap()], + &[ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("Editor").unwrap(), + ], + ); + assert_eq!( + describe_bindings(&bindings), + vec![ + ("ctrl-k b".to_string(), "test_only::ActionGamma"), + ("ctrl-k a".to_string(), "test_only::ActionAlpha"), + ] + ); + } + + #[test] + fn test_possible_next_bindings_exclude_longer_chord_disabled_in_deeper_context() { + let keymap = Keymap::new(vec![ + KeyBinding::new("space w w", ActionAlpha {}, Some("Workspace")), + KeyBinding::new("space w x", ActionBeta {}, Some("Workspace")), + KeyBinding::new("space w w", NoAction {}, Some("AgentPanel")), + ]); + let context_stack = [ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("AgentPanel").unwrap(), + ]; + + for input in ["space", "space w"] { + let input = input + .split(' ') + .map(|keystroke| Keystroke::parse(keystroke).unwrap()) + .collect::>(); + let bindings = keymap.possible_next_bindings_for_input(&input, &context_stack); + assert_eq!( + describe_bindings(&bindings), + vec![("space w x".to_string(), "test_only::ActionBeta")] + ); + } + } + + #[test] + fn test_possible_next_bindings_exclude_disabled_bindings_with_typed_input() { + // Option-S reports both its physical key and the character it produced, so typed input + // matches bindings written with either. + let typed_input = [Keystroke::parse("alt-s->ß").unwrap()]; + let keymap = Keymap::new(vec![ + KeyBinding::new("ß a", ActionAlpha {}, None), + KeyBinding::new("alt-s a", NoAction {}, None), + ]); + assert!( + keymap + .possible_next_bindings_for_input(&typed_input, &[]) + .is_empty() + ); + + // Cross-check against dispatch of the same typed input. + let typed_then_a = [ + Keystroke::parse("alt-s->ß").unwrap(), + Keystroke::parse("a").unwrap(), + ]; + let (dispatched, _) = keymap.bindings_for_input(&typed_then_a, &[]); + assert!(dispatched.is_empty()); + } + + fn describe_bindings(bindings: &[KeyBinding]) -> Vec<(String, &'static str)> { + bindings + .iter() + .map(|binding| { + ( + binding + .keystrokes() + .iter() + .map(|keystroke| keystroke.inner().unparse()) + .collect::>() + .join(" "), + binding.action().name(), + ) + }) + .collect() + } +} diff --git a/vendor/gpui-pre/src/keymap/binding.rs b/vendor/gpui-pre/src/keymap/binding.rs new file mode 100644 index 0000000..fc4b329 --- /dev/null +++ b/vendor/gpui-pre/src/keymap/binding.rs @@ -0,0 +1,143 @@ +use std::rc::Rc; + +use crate::{ + Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, + KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, +}; +use smallvec::SmallVec; + +/// A keybinding and its associated metadata, from the keymap. +pub struct KeyBinding { + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) context_predicate: Option>, + pub(crate) meta: Option, + /// The json input string used when building the keybinding, if any + pub(crate) action_input: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + meta: self.meta, + action_input: self.action_input.clone(), + } + } +} + +impl KeyBinding { + /// Construct a new keybinding from the given data. Panics on parse error. + pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { + let context_predicate = + context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); + Self::load( + keystrokes, + Box::new(action), + context_predicate, + false, + None, + &DummyKeyboardMapper, + ) + .unwrap() + } + + /// Load a keybinding from the given raw data. + pub fn load( + keystrokes: &str, + action: Box, + context_predicate: Option>, + use_key_equivalents: bool, + action_input: Option, + keyboard_mapper: &dyn PlatformKeyboardMapper, + ) -> std::result::Result { + let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + .split_whitespace() + .map(|source| { + let keystroke = Keystroke::parse(source)?; + Ok(KeybindingKeystroke::new_with_mapper( + keystroke, + use_key_equivalents, + keyboard_mapper, + )) + }) + .collect::>()?; + + Ok(Self { + keystrokes, + action, + context_predicate, + meta: None, + action_input, + }) + } + + /// Set the metadata for this binding. + pub fn with_meta(mut self, meta: KeyBindingMetaIndex) -> Self { + self.meta = Some(meta); + self + } + + /// Set the metadata for this binding. + pub fn set_meta(&mut self, meta: KeyBindingMetaIndex) { + self.meta = Some(meta); + } + + /// Check if the given keystrokes match this binding. + pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + if self.keystrokes.len() < typed.len() { + return None; + } + + for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { + if !typed.as_keystroke().should_match(target) { + return None; + } + } + + Some(self.keystrokes.len() > typed.len()) + } + + /// Get the keystrokes associated with this binding + pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + self.keystrokes.as_slice() + } + + /// Get the action associated with this binding + pub fn action(&self) -> &dyn Action { + self.action.as_ref() + } + + /// Get the predicate used to match this binding + pub fn predicate(&self) -> Option> { + self.context_predicate.as_ref().map(|rc| rc.clone()) + } + + /// Get the metadata for this binding + pub fn meta(&self) -> Option { + self.meta + } + + /// Get the action input associated with the action for this binding + pub fn action_input(&self) -> Option { + self.action_input.clone() + } +} + +impl std::fmt::Debug for KeyBinding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyBinding") + .field("keystrokes", &self.keystrokes) + .field("context_predicate", &self.context_predicate) + .field("action", &self.action.name()) + .finish() + } +} + +/// A unique identifier for retrieval of metadata associated with a key binding. +/// Intended to be used as an index or key into a user-defined store of metadata +/// associated with the binding, such as the source of the binding. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct KeyBindingMetaIndex(pub u32); diff --git a/vendor/gpui-pre/src/keymap/context.rs b/vendor/gpui-pre/src/keymap/context.rs new file mode 100644 index 0000000..27f361b --- /dev/null +++ b/vendor/gpui-pre/src/keymap/context.rs @@ -0,0 +1,891 @@ +use crate::SharedString; +use anyhow::{Context as _, Result}; +use std::fmt; + +/// A datastructure for resolving whether an action should be dispatched +/// at this point in the element tree. Contains a set of identifiers +/// and/or key value pairs representing the current context for the +/// keymap. +#[derive(Clone, Default, Eq, PartialEq, Hash)] +pub struct KeyContext(Vec); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +/// An entry in a KeyContext +pub struct ContextEntry { + /// The key (or name if no value) + pub key: SharedString, + /// The value + pub value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyContext { + /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`. + pub fn new_with_defaults() -> Self { + let mut context = Self::default(); + #[cfg(target_os = "macos")] + context.set("os", "macos"); + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + context.set("os", "linux"); + #[cfg(target_os = "windows")] + context.set("os", "windows"); + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "freebsd", + target_os = "windows" + )))] + context.set("os", "unknown"); + context + } + + /// Returns the primary context entry (usually the name of the component) + pub fn primary(&self) -> Option<&ContextEntry> { + self.0.iter().find(|p| p.value.is_none()) + } + + /// Returns everything except the primary context entry. + pub fn secondary(&self) -> impl Iterator { + let primary = self.primary(); + self.0.iter().filter(move |&p| Some(p) != primary) + } + + /// Parse a key context from a string. + /// The key context format is very simple: + /// - either a single identifier, such as `StatusBar` + /// - or a key value pair, such as `mode = visible` + /// - separated by whitespace, such as `StatusBar mode = visible` + pub fn parse(source: &str) -> Result { + let mut context = Self::default(); + let source = skip_whitespace(source); + Self::parse_expr(source, &mut context)?; + Ok(context) + } + + fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { + if source.is_empty() { + return Ok(()); + } + + let key = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[key.len()..]); + if let Some(suffix) = source.strip_prefix('=') { + source = skip_whitespace(suffix); + let value = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[value.len()..]); + context.set(key, value); + } else { + context.add(key); + } + + Self::parse_expr(source, context) + } + + /// Check if this context is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Clear this context. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Extend this context with another context. + pub fn extend(&mut self, other: &Self) { + for entry in &other.0 { + if !self.contains(&entry.key) { + self.0.push(entry.clone()); + } + } + } + + /// Add an identifier to this context, if it's not already in this context. + pub fn add>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + /// Set a key value pair in this context, if it's not already set. + pub fn set, S2: Into>(&mut self, key: S1, value: S2) { + let key = key.into(); + if !self.contains(&key) { + self.0.push(ContextEntry { + key, + value: Some(value.into()), + }) + } + } + + /// Check if this context contains a given identifier or key. + pub fn contains(&self, key: &str) -> bool { + self.0.iter().any(|entry| entry.key.as_ref() == key) + } + + /// Get the associated value for a given identifier or key. + pub fn get(&self, key: &str) -> Option<&SharedString> { + self.0 + .iter() + .find(|entry| entry.key.as_ref() == key)? + .value + .as_ref() + } +} + +impl fmt::Debug for KeyContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = self.0.iter().peekable(); + while let Some(entry) = entries.next() { + if let Some(ref value) = entry.value { + write!(f, "{}={}", entry.key, value)?; + } else { + write!(f, "{}", entry.key)?; + } + if entries.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + +/// A datastructure for resolving whether an action should be dispatched +/// Representing a small language for describing which contexts correspond +/// to which actions. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeyBindingContextPredicate { + /// A predicate that will match a given identifier. + Identifier(SharedString), + /// A predicate that will match a given key-value pair. + Equal(SharedString, SharedString), + /// A predicate that will match a given key-value pair not being present. + NotEqual(SharedString, SharedString), + /// A predicate that will match a given predicate appearing below another predicate. + /// in the element tree + Descendant( + Box, + Box, + ), + /// Predicate that will invert another predicate. + Not(Box), + /// A predicate that will match if both of its children match. + And( + Box, + Box, + ), + /// A predicate that will match if either of its children match. + Or( + Box, + Box, + ), +} + +impl fmt::Display for KeyBindingContextPredicate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Identifier(name) => write!(f, "{name}"), + Self::Equal(left, right) => write!(f, "{left} == {right}"), + Self::NotEqual(left, right) => write!(f, "{left} != {right}"), + Self::Descendant(parent, child) => write!(f, "{parent} > {child}"), + Self::Not(pred) => match pred.as_ref() { + Self::Identifier(name) => write!(f, "!{name}"), + _ => write!(f, "!({pred})"), + }, + Self::And(..) => self.fmt_joined(f, " && ", LogicalOperator::And, |node| { + matches!(node, Self::Or(..)) + }), + Self::Or(..) => self.fmt_joined(f, " || ", LogicalOperator::Or, |node| { + matches!(node, Self::And(..)) + }), + } + } +} + +impl KeyBindingContextPredicate { + /// Parse a string in the same format as the keymap's context field. + /// + /// A basic equivalence check against a set of identifiers can performed by + /// simply writing a string: + /// + /// `StatusBar` -> A predicate that will match a context with the identifier `StatusBar` + /// + /// You can also specify a key-value pair: + /// + /// `mode == visible` -> A predicate that will match a context with the key `mode` + /// with the value `visible` + /// + /// And a logical operations combining these two checks: + /// + /// `StatusBar && mode == visible` -> A predicate that will match a context with the + /// identifier `StatusBar` and the key `mode` + /// with the value `visible` + /// + /// + /// There is also a special child `>` operator that will match a predicate that is + /// below another predicate: + /// + /// `StatusBar > mode == visible` -> A predicate that will match a context identifier `StatusBar` + /// and a child context that has the key `mode` with the + /// value `visible` + /// + /// This syntax supports `!=`, `||` and `&&` as logical operators. + /// You can also preface an operation or check with a `!` to negate it. + pub fn parse(source: &str) -> Result { + let source = skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + anyhow::bail!("unexpected character '{next:?}'"); + } else { + Ok(predicate) + } + } + + /// Find the deepest depth at which the predicate matches. + pub fn depth_of(&self, contexts: &[KeyContext]) -> Option { + for depth in (0..=contexts.len()).rev() { + let context_slice = &contexts[0..depth]; + if self.eval_inner(context_slice, contexts) { + return Some(depth); + } + } + None + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + #[allow(unused)] + pub fn eval(&self, contexts: &[KeyContext]) -> bool { + self.eval_inner(contexts, contexts) + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool { + let Some(context) = contexts.last() else { + return false; + }; + match self { + Self::Identifier(name) => context.contains(name), + Self::Equal(left, right) => context + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => { + for i in 0..all_contexts.len() { + if pred.eval_inner(&all_contexts[..=i], all_contexts) { + return false; + } + } + true + } + // Workspace > Pane > Editor + // + // Pane > (Pane > Editor) // should match? + // (Pane > Pane) > Editor // should not match? + // Pane > !Workspace <-- should match? + // !Workspace <-- shouldn't match? + Self::Descendant(parent, child) => { + for i in 0..contexts.len() - 1 { + // [Workspace > Pane], [Editor] + if parent.eval_inner(&contexts[..=i], all_contexts) { + if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) { + return false; + } + return true; + } + } + false + } + Self::And(left, right) => { + left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) + } + Self::Or(left, right) => { + left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts) + } + } + } + + /// Returns whether or not this predicate matches all possible contexts matched by + /// the other predicate. + pub fn is_superset(&self, other: &Self) -> bool { + if self == other { + return true; + } + + if let KeyBindingContextPredicate::Or(left, right) = self { + return left.is_superset(other) || right.is_superset(other); + } + + match other { + KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child), + KeyBindingContextPredicate::And(left, right) => { + self.is_superset(left) || self.is_superset(right) + } + KeyBindingContextPredicate::Identifier(_) => false, + KeyBindingContextPredicate::Equal(_, _) => false, + KeyBindingContextPredicate::NotEqual(_, _) => false, + KeyBindingContextPredicate::Not(_) => false, + KeyBindingContextPredicate::Or(_, _) => false, + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = fn( + KeyBindingContextPredicate, + KeyBindingContextPredicate, + ) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source.chars().next().context("unexpected end")?; + match next { + '(' => { + source = skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + let stripped = rest.strip_prefix(')').context("expected a ')'")?; + source = skip_whitespace(stripped); + Ok((predicate, source)) + } + '!' => { + let source = skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(source, PRECEDENCE_NOT)?; + Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) + } + _ if is_identifier_char(next) => { + let len = source + .find(|c: char| !is_identifier_char(c) && !is_vim_operator_char(c)) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(identifier.to_string().into()), + source, + )) + } + _ if is_vim_operator_char(next) => { + let (operator, rest) = source.split_at(1); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(operator.to_string().into()), + source, + )) + } + _ => anyhow::bail!("unexpected character '{next:?}'"), + } + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Descendant(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + anyhow::bail!("operands of == must be identifiers"); + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + anyhow::bail!("operands of != must be identifiers"); + } + } + + fn fmt_joined( + &self, + f: &mut fmt::Formatter<'_>, + separator: &str, + operator: LogicalOperator, + needs_parens: impl Fn(&Self) -> bool + Copy, + ) -> fmt::Result { + let mut first = true; + self.fmt_joined_inner(f, separator, operator, needs_parens, &mut first) + } + + fn fmt_joined_inner( + &self, + f: &mut fmt::Formatter<'_>, + separator: &str, + operator: LogicalOperator, + needs_parens: impl Fn(&Self) -> bool + Copy, + first: &mut bool, + ) -> fmt::Result { + match (operator, self) { + (LogicalOperator::And, Self::And(left, right)) + | (LogicalOperator::Or, Self::Or(left, right)) => { + left.fmt_joined_inner(f, separator, operator, needs_parens, first)?; + right.fmt_joined_inner(f, separator, operator, needs_parens, first) + } + (_, node) => { + if !*first { + f.write_str(separator)?; + } + *first = false; + + if needs_parens(node) { + write!(f, "({node})") + } else { + write!(f, "{node}") + } + } + } + } +} + +#[derive(Clone, Copy)] +enum LogicalOperator { + And, + Or, +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +fn is_vim_operator_char(c: char) -> bool { + c == '>' || c == '<' || c == '~' || c == '"' || c == '?' +} + +fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] +} + +#[cfg(test)] +mod tests { + use core::slice; + + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; + + #[test] + fn test_actions_definition() { + { + actions!(test_only, [A, B, C, D, E, F, G]); + } + + { + actions!( + test_only, + [ + H, I, J, K, L, M, N, // Don't wrap, test the trailing comma + ] + ); + } + } + + #[test] + fn test_parse_context() { + let mut expected = KeyContext::default(); + expected.add("baz"); + expected.set("foo", "bar"); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); + assert_eq!( + KeyContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); + } + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeyBindingContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeyBindingContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands of == must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } + + #[test] + fn test_is_superset() { + assert_is_superset("editor", "editor", true); + assert_is_superset("editor", "workspace", false); + + assert_is_superset("editor", "editor && vim_mode", true); + assert_is_superset("editor", "mode == full && editor", true); + assert_is_superset("editor && mode == full", "editor", false); + + assert_is_superset("editor", "something > editor", true); + assert_is_superset("editor", "editor > menu", false); + + assert_is_superset("foo || bar || baz", "bar", true); + assert_is_superset("foo || bar || baz", "quux", false); + + #[track_caller] + fn assert_is_superset(a: &str, b: &str, result: bool) { + let a = KeyBindingContextPredicate::parse(a).unwrap(); + let b = KeyBindingContextPredicate::parse(b).unwrap(); + assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})"); + } + } + + #[test] + fn test_child_operator() { + let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap(); + + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + let contexts = vec![parent_context.clone(), child_context.clone()]; + assert!(predicate.eval(&contexts)); + + let grandparent_context = KeyContext::try_from("grandparent").unwrap(); + + let contexts = vec![ + grandparent_context, + parent_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); + + let other_context = KeyContext::try_from("other").unwrap(); + + let contexts = vec![other_context.clone(), child_context.clone()]; + assert!(!predicate.eval(&contexts)); + + let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; + assert!(predicate.eval(&contexts)); + + assert!(!predicate.eval(&[])); + assert!(!predicate.eval(slice::from_ref(&child_context))); + assert!(!predicate.eval(&[parent_context])); + + let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); + assert!(!zany_predicate.eval(slice::from_ref(&child_context))); + assert!(zany_predicate.eval(&[child_context.clone(), child_context])); + } + + #[test] + fn test_not_operator() { + let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap(); + let editor_context = KeyContext::try_from("editor").unwrap(); + let workspace_context = KeyContext::try_from("workspace").unwrap(); + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + assert!(not_predicate.eval(slice::from_ref(&workspace_context))); + assert!(!not_predicate.eval(slice::from_ref(&editor_context))); + assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); + assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); + + let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); + assert!(complex_not.eval(slice::from_ref(&workspace_context))); + assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); + + let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); + let mut mode_context = KeyContext::default(); + mode_context.set("mode", "full"); + assert!(!not_mode_predicate.eval(&[mode_context.clone()])); + + let mut other_mode_context = KeyContext::default(); + other_mode_context.set("mode", "partial"); + assert!(not_mode_predicate.eval(&[other_mode_context])); + + let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); + assert!(not_descendant.eval(slice::from_ref(&parent_context))); + assert!(not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); + + let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); + assert!(!not_descendant.eval(slice::from_ref(&parent_context))); + assert!(!not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context, child_context])); + + let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); + assert!(double_not.eval(slice::from_ref(&editor_context))); + assert!(!double_not.eval(slice::from_ref(&workspace_context))); + + // Test complex descendant cases + let workspace_context = KeyContext::try_from("Workspace").unwrap(); + let pane_context = KeyContext::try_from("Pane").unwrap(); + let editor_context = KeyContext::try_from("Editor").unwrap(); + + // Workspace > Pane > Editor + let workspace_pane_editor = vec![ + workspace_context.clone(), + pane_context.clone(), + editor_context.clone(), + ]; + + // Pane > (Pane > Editor) - should not match + let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap(); + assert!(!pane_pane_editor.eval(&workspace_pane_editor)); + + let workspace_pane_editor_predicate = + KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap(); + assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor)); + + // (Pane > Pane) > Editor - should not match + let pane_pane_then_editor = + KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap(); + assert!(!pane_pane_then_editor.eval(&workspace_pane_editor)); + + // Pane > !Workspace - should match + let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap(); + assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()])); + assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()])); + + // !Workspace - shouldn't match when Workspace is in the context + let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); + assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); + assert!(not_workspace.eval(slice::from_ref(&pane_context))); + assert!(not_workspace.eval(slice::from_ref(&editor_context))); + assert!(!not_workspace.eval(&workspace_pane_editor)); + } + + // MARK: - Display + + #[test] + fn test_context_display() { + fn ident(s: &str) -> Box { + Box::new(Identifier(SharedString::new(s))) + } + fn eq(a: &str, b: &str) -> Box { + Box::new(Equal(SharedString::new(a), SharedString::new(b))) + } + fn not_eq(a: &str, b: &str) -> Box { + Box::new(NotEqual(SharedString::new(a), SharedString::new(b))) + } + fn and( + a: Box, + b: Box, + ) -> Box { + Box::new(And(a, b)) + } + fn or( + a: Box, + b: Box, + ) -> Box { + Box::new(Or(a, b)) + } + fn descendant( + a: Box, + b: Box, + ) -> Box { + Box::new(Descendant(a, b)) + } + fn not(a: Box) -> Box { + Box::new(Not(a)) + } + + let test_cases = [ + (ident("a"), "a"), + (eq("a", "b"), "a == b"), + (not_eq("a", "b"), "a != b"), + (descendant(ident("a"), ident("b")), "a > b"), + (not(ident("a")), "!a"), + (not_eq("a", "b"), "a != b"), + (descendant(ident("a"), ident("b")), "a > b"), + (not(and(ident("a"), ident("b"))), "!(a && b)"), + (not(or(ident("a"), ident("b"))), "!(a || b)"), + (and(ident("a"), ident("b")), "a && b"), + (and(and(ident("a"), ident("b")), ident("c")), "a && b && c"), + (or(ident("a"), ident("b")), "a || b"), + (or(or(ident("a"), ident("b")), ident("c")), "a || b || c"), + (or(ident("a"), and(ident("b"), ident("c"))), "a || (b && c)"), + ( + and( + and( + and(ident("a"), eq("b", "c")), + not(descendant(ident("d"), ident("e"))), + ), + eq("f", "g"), + ), + "a && b == c && !(d > e) && f == g", + ), + ( + and(and(ident("a"), or(ident("b"), ident("c"))), ident("d")), + "a && (b || c) && d", + ), + ( + or(or(ident("a"), and(ident("b"), ident("c"))), ident("d")), + "a || (b && c) || d", + ), + ]; + + for (predicate, expected) in test_cases { + let actual = predicate.to_string(); + assert_eq!(actual, expected); + let parsed = KeyBindingContextPredicate::parse(&actual).unwrap(); + assert_eq!(parsed, *predicate); + } + } +} diff --git a/vendor/gpui-pre/src/path_builder.rs b/vendor/gpui-pre/src/path_builder.rs new file mode 100644 index 0000000..40a6e71 --- /dev/null +++ b/vendor/gpui-pre/src/path_builder.rs @@ -0,0 +1,347 @@ +use anyhow::Error; +use etagere::euclid::{Point2D, Vector2D}; +use lyon::geom::Angle; +use lyon::math::{Vector, vector}; +use lyon::path::traits::SvgPathBuilder; +use lyon::path::{ArcFlags, Polygon}; +use lyon::tessellation::{ + BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers, +}; + +pub use lyon::math::Transform; +pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions}; + +use crate::{Path, Pixels, Point, point, px}; + +/// Style of the PathBuilder +pub enum PathStyle { + /// Stroke style + Stroke(StrokeOptions), + /// Fill style + Fill(FillOptions), +} + +/// A [`Path`] builder. +pub struct PathBuilder { + raw: lyon::path::builder::WithSvg, + transform: Option, + /// PathStyle of the PathBuilder + pub style: PathStyle, + dash_array: Option>, +} + +impl From for PathBuilder { + fn from(builder: lyon::path::Builder) -> Self { + Self { + raw: builder.with_svg(), + ..Default::default() + } + } +} + +impl From> for PathBuilder { + fn from(raw: lyon::path::builder::WithSvg) -> Self { + Self { + raw, + ..Default::default() + } + } +} + +impl From for Point { + fn from(p: lyon::math::Point) -> Self { + point(px(p.x), px(p.y)) + } +} + +impl From> for lyon::math::Point { + fn from(p: Point) -> Self { + lyon::math::point(p.x.0, p.y.0) + } +} + +impl From> for Vector { + fn from(p: Point) -> Self { + vector(p.x.0, p.y.0) + } +} + +impl From> for Point2D { + fn from(p: Point) -> Self { + Point2D::new(p.x.0, p.y.0) + } +} + +impl Default for PathBuilder { + fn default() -> Self { + Self { + raw: lyon::path::Path::builder().with_svg(), + style: PathStyle::Fill(FillOptions::default()), + transform: None, + dash_array: None, + } + } +} + +impl PathBuilder { + /// Creates a new [`PathBuilder`] to build a Stroke path. + pub fn stroke(width: Pixels) -> Self { + Self { + style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)), + ..Self::default() + } + } + + /// Creates a new [`PathBuilder`] to build a Fill path. + pub fn fill() -> Self { + Self::default() + } + + /// Sets the style of the [`PathBuilder`]. + pub fn with_style(self, style: PathStyle) -> Self { + Self { style, ..self } + } + + /// Sets the dash array of the [`PathBuilder`]. + /// + /// [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/stroke-dasharray) + pub fn dash_array(mut self, dash_array: &[Pixels]) -> Self { + // If an odd number of values is provided, then the list of values is repeated to yield an even number of values. + // Thus, 5,3,2 is equivalent to 5,3,2,5,3,2. + let array = if dash_array.len() % 2 == 1 { + let mut new_dash_array = dash_array.to_vec(); + new_dash_array.extend_from_slice(dash_array); + new_dash_array + } else { + dash_array.to_vec() + }; + + self.dash_array = Some(array); + self + } + + /// Move the current point to the given point. + #[inline] + pub fn move_to(&mut self, to: Point) { + self.raw.move_to(to.into()); + } + + /// Draw a straight line from the current point to the given point. + #[inline] + pub fn line_to(&mut self, to: Point) { + self.raw.line_to(to.into()); + } + + /// Draw a curve from the current point to the given point, using the given control point. + #[inline] + pub fn curve_to(&mut self, to: Point, ctrl: Point) { + self.raw.quadratic_bezier_to(ctrl.into(), to.into()); + } + + /// Adds a cubic Bézier to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn cubic_bezier_to( + &mut self, + to: Point, + control_a: Point, + control_b: Point, + ) { + self.raw + .cubic_bezier_to(control_a.into(), control_b.into(), to.into()); + } + + /// Adds an elliptical arc. + pub fn arc_to( + &mut self, + radii: Point, + x_rotation: Pixels, + large_arc: bool, + sweep: bool, + to: Point, + ) { + self.raw.arc_to( + radii.into(), + Angle::degrees(x_rotation.into()), + ArcFlags { large_arc, sweep }, + to.into(), + ); + } + + /// Equivalent to `arc_to` in relative coordinates. + pub fn relative_arc_to( + &mut self, + radii: Point, + x_rotation: Pixels, + large_arc: bool, + sweep: bool, + to: Point, + ) { + self.raw.relative_arc_to( + radii.into(), + Angle::degrees(x_rotation.into()), + ArcFlags { large_arc, sweep }, + to.into(), + ); + } + + /// Adds a polygon. + pub fn add_polygon(&mut self, points: &[Point], closed: bool) { + let points = points.iter().copied().map(|p| p.into()).collect::>(); + self.raw.add_polygon(Polygon { + points: points.as_ref(), + closed, + }); + } + + /// Close the current sub-path. + #[inline] + pub fn close(&mut self) { + self.raw.close(); + } + + /// Applies a transform to the path. + #[inline] + pub fn transform(&mut self, transform: Transform) { + self.transform = Some(transform); + } + + /// Applies a translation to the path. + #[inline] + pub fn translate(&mut self, to: Point) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0))); + } else { + self.transform = Some(Transform::translation(to.x.0, to.y.0)) + } + } + + /// Applies a scale to the path. + #[inline] + pub fn scale(&mut self, scale: f32) { + if let Some(transform) = self.transform { + self.transform = Some(transform.then_scale(scale, scale)); + } else { + self.transform = Some(Transform::scale(scale, scale)); + } + } + + /// Applies a rotation to the path. + /// + /// The `angle` is in degrees value in the range 0.0 to 360.0. + #[inline] + pub fn rotate(&mut self, angle: f32) { + let radians = angle.to_radians(); + if let Some(transform) = self.transform { + self.transform = Some(transform.then_rotate(Angle::radians(radians))); + } else { + self.transform = Some(Transform::rotation(Angle::radians(radians))); + } + } + + /// Builds into a [`Path`]. + #[inline] + pub fn build(self) -> Result, Error> { + let path = if let Some(transform) = self.transform { + self.raw.build().transformed(&transform) + } else { + self.raw.build() + }; + + match self.style { + PathStyle::Stroke(options) => Self::tessellate_stroke(self.dash_array, &path, &options), + PathStyle::Fill(options) => Self::tessellate_fill(&path, &options), + } + } + + fn tessellate_fill( + path: &lyon::path::Path, + options: &FillOptions, + ) -> Result, Error> { + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = FillTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + fn tessellate_stroke( + dash_array: Option>, + path: &lyon::path::Path, + options: &StrokeOptions, + ) -> Result, Error> { + let path = if let Some(dash_array) = dash_array { + let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01); + let mut sampler = measurements + .create_sampler(path, lyon::algorithms::measure::SampleType::Normalized); + let mut builder = lyon::path::Path::builder(); + + let total_length = sampler.length(); + let dash_array_len = dash_array.len(); + let mut pos = 0.; + let mut dash_index = 0; + while pos < total_length { + let dash_length = dash_array[dash_index % dash_array_len].0; + let next_pos = (pos + dash_length).min(total_length); + if dash_index % 2 == 0 { + let start = pos / total_length; + let end = next_pos / total_length; + sampler.split_range(start..end, &mut builder); + } + pos = next_pos; + dash_index += 1; + } + + &builder.build() + } else { + path + }; + + // Will contain the result of the tessellation. + let mut buf: VertexBuffers = VertexBuffers::new(); + let mut tessellator = StrokeTessellator::new(); + + // Compute the tessellation. + tessellator.tessellate_path( + path, + options, + &mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()), + )?; + + Ok(Self::build_path(buf)) + } + + /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`]. + pub fn build_path(buf: VertexBuffers) -> Path { + if buf.vertices.is_empty() { + return Path::new(Point::default()); + } + + let first_point = buf.vertices[0]; + + let mut path = Path::new(first_point.into()); + for i in 0..buf.indices.len() / 3 { + let i0 = buf.indices[i * 3] as usize; + let i1 = buf.indices[i * 3 + 1] as usize; + let i2 = buf.indices[i * 3 + 2] as usize; + + let v0 = buf.vertices[i0]; + let v1 = buf.vertices[i1]; + let v2 = buf.vertices[i2]; + + path.push_triangle( + (v0.into(), v1.into(), v2.into()), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + + path + } +} diff --git a/vendor/gpui-pre/src/platform.rs b/vendor/gpui-pre/src/platform.rs new file mode 100644 index 0000000..e47fd00 --- /dev/null +++ b/vendor/gpui-pre/src/platform.rs @@ -0,0 +1,3766 @@ +mod app_menu; +mod keyboard; +mod keystroke; + +#[cfg(all(target_os = "linux", feature = "wayland"))] +#[expect(missing_docs)] +pub mod layer_shell; + +/// Types for configuring parent-anchored popup windows such as menus, dropdowns and tooltips. +pub mod popup; + +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +mod threaded_dispatcher; + +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +mod test; + +#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] +mod visual_test; + +#[cfg(all( + feature = "screen-capture", + any(target_os = "windows", target_os = "linux", target_os = "freebsd",) +))] +pub mod scap_screen_capture; + +#[cfg(all( + any(target_os = "windows", target_os = "linux"), + feature = "screen-capture" +))] +pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame; +#[cfg(not(feature = "screen-capture"))] +pub(crate) type PlatformScreenCaptureFrame = (); +#[cfg(all(target_os = "macos", feature = "screen-capture"))] +pub(crate) type PlatformScreenCaptureFrame = + objc2_core_foundation::CFRetained; + +use crate::{ + Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, + DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font, + FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, + LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority, + RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, + ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, + hash, point, px, size, +}; +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +use anyhow::bail; +use anyhow::{Context as _, Result}; +use async_task::Runnable; +use collections::FxHashMap; +use futures::channel::oneshot; +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +use image::RgbaImage; +use image::codecs::gif::GifDecoder; +use image::{AnimationDecoder as _, DynamicImage, Frame}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use scheduler::Instant; +pub use scheduler::RunnableMeta; +use schemars::JsonSchema; +use seahash::SeaHasher; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::collections::hash_map::Entry; +use std::hash::{Hash, Hasher}; +use std::io::Cursor; +use std::ops; +use std::time::Duration; +use std::{ + ffi::OsString, + fmt::{self, Debug}, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; +use strum::EnumIter; +use uuid::Uuid; + +pub use app_menu::*; +pub use keyboard::*; +pub use keystroke::*; + +/// Whether the platform is presenting a window's frames. +/// +/// This is about presentation, not the window's shown/hidden state: a shown +/// window that is fully covered by other windows, minimized, on another +/// Space or virtual desktop, or on a display that is asleep is `Hidden`. A +/// window only partly covered by other windows is `Visible`. +/// +/// Each platform reports from a single source, and what that source can see +/// differs: +/// +/// * macOS: `NSWindow.occlusionState`. Covers all of the cases above. +/// * Windows: `WS_VISIBLE` and the minimized state. Windows keeps compositing +/// covered windows for thumbnails and Alt-Tab and offers no occlusion +/// notification, so a fully covered window stays `Visible`. Display sleep is +/// not reported either. +/// * Wayland: the `xdg_toplevel` `suspended` state (xdg-shell v6). Compositors +/// that don't support it never report `Hidden`. +/// * X11: mapped state plus `VisibilityNotify`. Compositing window managers +/// generally never report a window as fully obscured, so covering is only +/// detected without compositing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowVisibility { + /// At least part of the window is being presented; frames drawn for it + /// will be shown. + Visible, + /// No part of the window is being presented. The platform will not + /// request frames for it until it becomes visible again. + Hidden, +} + +impl WindowVisibility { + /// Whether frames drawn for the window will be shown. + pub fn is_visible(self) -> bool { + self == Self::Visible + } +} + +/// Controls whether the application participates in the system's foreground UI. +/// +/// Only has an effect on macOS; other platforms ignore this setting. There, +/// [`App::request_windowing`] sets it: `Accessory` while headless and `Regular` while windowed. +/// Set it directly with [`App::set_activation_policy`] for the one case that doesn't cover: an +/// accessory app that shows windows, such as a menu bar utility. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum ActivationPolicy { + /// Participate in foreground application UI, such as the Dock and menu bar on macOS. + #[default] + Regular, + /// Run without foreground application UI while retaining the ability to open windows. + Accessory, +} + +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +pub(crate) use test::*; + +#[cfg(any(test, feature = "test-support"))] +pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; + +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +pub use threaded_dispatcher::ThreadedDispatcher; + +#[cfg(all(target_os = "macos", any(test, feature = "test-support")))] +pub use visual_test::VisualTestPlatform; + +/// Keeps an operating system activity, such as an idle sleep inhibitor, alive until dropped. +pub struct ActivityGuard { + _release: gpui_util::Deferred>, +} + +impl ActivityGuard { + /// Runs `release` when the guard is dropped. + pub fn new(release: impl FnOnce() + Send + 'static) -> Self { + Self { + _release: gpui_util::defer(Box::new(release)), + } + } + + /// A guard for platforms without a corresponding activity. + pub fn noop() -> Self { + Self::new(|| {}) + } +} + +// TODO(jk): return an enum instead of a string +/// Return which compositor we're guessing we'll use. +/// Does not attempt to connect to the given compositor. +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +#[inline] +pub fn guess_compositor() -> &'static str { + if std::env::var_os("ZED_HEADLESS").is_some() { + return "Headless"; + } + GraphicalEnvironment::detect().guess_compositor() +} + +/// The graphical session to connect to: the variables that locate its display server, as some +/// process sees them. +/// +/// A long-running process can outlive the graphical session it was started in, so a platform +/// that attaches to a display server later can be given a fresher environment than its own. +/// While connected, programs the platform launches (for example to open a URL) get these +/// variables instead of the ones this process started with. Apply +/// [`App::graphical_environment`] to the programs an app launches, for the same reason. +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +#[derive(Clone, Debug, Default)] +pub struct GraphicalEnvironment { + /// `WAYLAND_DISPLAY`: a socket name relative to `xdg_runtime_dir`, or an absolute path. + pub wayland_display: Option, + /// `DISPLAY`: the X11 display name. + pub x11_display: Option, + /// `XDG_RUNTIME_DIR`: the directory containing Wayland sockets. + pub xdg_runtime_dir: Option, + /// `XDG_ACTIVATION_TOKEN`: lets the first window take focus on Wayland. + /// + /// A platform takes this process's token from its environment when it's created, and uses + /// it if it starts windowed. Pass one here to switch to windowed mode later, for example + /// the token of the process that asked for a window. + pub activation_token: Option, +} + +/// The graphical session to connect to: the Windows session whose desktop windows appear on. +/// +/// A process can only show windows in its own session, so switching to windowed mode fails if +/// this names another one, for example when a process started over SSH is asked to show a +/// window on the desktop. +#[cfg(target_os = "windows")] +#[derive(Clone, Debug, Default)] +pub struct GraphicalEnvironment { + /// The session ID, as `ProcessIdToSessionId` reports it. `None` means this process's own + /// session. + pub session_id: Option, +} + +#[cfg(target_os = "windows")] +impl GraphicalEnvironment { + /// Returns the environment of this process's session. + pub fn detect() -> Self { + let mut session_id = 0; + // SAFETY: `session_id` is a valid pointer for the call's duration. + let result = unsafe { + windows::Win32::System::RemoteDesktop::ProcessIdToSessionId( + windows::Win32::System::Threading::GetCurrentProcessId(), + &mut session_id, + ) + }; + Self { + session_id: result.is_ok().then_some(session_id), + } + } + + /// Does nothing on this platform: programs inherit the session of the process that + /// starts them. + pub fn apply_to(&self, _command: &mut std::process::Command) {} +} + +/// The graphical session to connect to. Carries nothing yet on this platform. +#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] +#[derive(Clone, Debug, Default)] +pub struct GraphicalEnvironment; + +#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))] +impl GraphicalEnvironment { + /// Returns the environment of this process's graphical session. + pub fn detect() -> Self { + Self + } + + /// Does nothing on this platform. + pub fn apply_to(&self, _command: &mut std::process::Command) {} +} + +/// A display mode for [`App::request_windowing`] to switch to. +#[derive(Clone, Debug)] +pub enum WindowingRequest { + /// No display server. Windows lay out and handle input but draw nothing. + Headless, + /// Connected to the display server that the environment names. + Windowed(GraphicalEnvironment), +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +impl GraphicalEnvironment { + /// Reads the display variables from this process's environment. + /// + /// This only reads environment variables: whether they name a reachable display server is + /// checked when a platform connects. Leaves `activation_token` unset: see its documentation. + pub fn detect() -> Self { + Self { + wayland_display: std::env::var_os("WAYLAND_DISPLAY"), + x11_display: std::env::var_os("DISPLAY"), + xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"), + activation_token: None, + } + } + + /// Sets this environment's display variables on `command`, and removes the ones it doesn't + /// set, so the program connects to this graphical session rather than the one this process + /// started in. Leaves `XDG_ACTIVATION_TOKEN` alone. + pub fn apply_to(&self, command: &mut std::process::Command) { + for (name, value) in [ + ("WAYLAND_DISPLAY", &self.wayland_display), + ("DISPLAY", &self.x11_display), + ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir), + ] { + match value { + Some(value) => command.env(name, value), + None => command.env_remove(name), + }; + } + } + + /// Returns the compositor this environment selects: Wayland, then X11, then headless. + /// + /// Does not attempt to connect to the compositor. + pub fn guess_compositor(&self) -> &'static str { + let is_set = + |value: &Option| value.as_ref().is_some_and(|value| !value.is_empty()); + if cfg!(feature = "wayland") && is_set(&self.wayland_display) { + "Wayland" + } else if cfg!(feature = "x11") && is_set(&self.x11_display) { + "X11" + } else { + "Headless" + } + } +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +bitflags::bitflags! { + /// The windowing modes a platform may start in or switch to. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct WindowingModes: u8 { + /// Connected to a Wayland compositor. + const WAYLAND = 1 << 0; + /// Connected to an X server. + const X11 = 1 << 1; + /// No display server. Windows lay out and handle input but draw nothing. + const HEADLESS = 1 << 2; + } +} + +#[expect(missing_docs)] +pub trait Platform: 'static { + fn background_executor(&self) -> BackgroundExecutor; + fn foreground_executor(&self) -> ForegroundExecutor; + fn text_system(&self) -> Arc; + + fn run(&self, on_finish_launching: Box); + fn quit(&self); + /// Switches a capable platform between headless and windowed modes. See + /// [`App::request_windowing`]. + /// Sets the windowing mode the platform starts in. Called before `run`. See + /// [`Application::with_windowing`]. + fn set_initial_windowing(&self, _request: WindowingRequest) {} + fn request_windowing(&self, _request: WindowingRequest) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "this platform cannot switch between headless and windowed modes" + ))) + } + fn restart(&self, binary_path: Option, arguments: Vec); + fn activate(&self, ignoring_other_apps: bool); + /// Sets the initial or current activation policy. Has no effect outside macOS. + fn set_activation_policy(&self, _policy: ActivationPolicy) {} + fn hide(&self); + fn hide_other_apps(&self); + fn unhide_other_apps(&self); + + fn displays(&self) -> Vec>; + fn primary_display(&self) -> Option>; + fn active_window(&self) -> Option; + fn window_stack(&self) -> Option> { + None + } + + fn is_screen_capture_supported(&self) -> bool { + false + } + + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + sources_tx + .send(Err(anyhow::anyhow!( + "gpui was compiled without the screen-capture feature" + ))) + .ok(); + sources_rx + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> anyhow::Result>; + + /// Returns the appearance of the application's windows. + fn window_appearance(&self) -> WindowAppearance; + + /// Overrides the appearance (light/dark) applied to the app's windows, independent + /// of the OS-wide setting. Pass `None` to clear the override and follow the system + /// again. The override is reflected by [`Platform::window_appearance`]. + /// + /// Currently only implemented on macOS, where it sets `NSApplication.appearance` so + /// the native window chrome (the window border and titlebar) of every window matches + /// a dark app theme even when the system is in light mode (or vice versa). A no-op on + /// other platforms. + fn set_window_appearance(&self, _appearance: Option) {} + + /// Returns the window button layout configuration when supported. + fn button_layout(&self) -> Option { + None + } + + fn open_url(&self, url: &str); + fn on_open_urls(&self, callback: Box)>); + fn register_url_scheme(&self, url: &str) -> Task>; + + fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>>>; + fn prompt_for_new_path( + &self, + directory: &Path, + suggested_name: Option<&str>, + ) -> oneshot::Receiver>>; + fn can_select_mixed_files_and_dirs(&self) -> bool; + fn reveal_path(&self, path: &Path); + fn open_with_system(&self, path: &Path); + + fn on_quit(&self, callback: Box bool>); + fn on_reopen(&self, callback: Box); + /// Registers the callback invoked when the system is about to sleep. + fn on_system_sleep(&self, callback: Box); + /// Registers the callback invoked when the system resumes from sleep. + fn on_system_wake(&self, callback: Box); + + // Mobile platform methods. On mobile the OS owns the application + // lifecycle: apps are backgrounded, foregrounded, and killed at the + // system's discretion, and must react rather than decide. + + /// Registers a callback invoked whenever the application's lifecycle + /// phase changes. See [`AppLifecyclePhase`] for the phase vocabulary and + /// its mapping onto iOS and Android. + /// + /// Desktop platforms never invoke this. + fn on_app_lifecycle(&self, _callback: Box) {} + + /// Registers a callback invoked when the OS signals memory pressure + /// (iOS `didReceiveMemoryWarning`, Android `onTrimMemory`). + /// + /// Desktop platforms never invoke this. + fn on_memory_warning(&self, _callback: Box) {} + + /// The platform's gesture recognition services, if it provides any + /// beyond gpui's portable recognizers. See + /// [`PlatformGestures`](crate::PlatformGestures). + fn gestures(&self) -> Option> { + None + } + + fn set_menus(&self, menus: Vec, keymap: &Keymap); + fn get_menus(&self) -> Option> { + None + } + + fn set_dock_menu(&self, menu: Vec, keymap: &Keymap); + fn perform_dock_menu_action(&self, _action: usize) {} + fn add_recent_document(&self, _path: &Path) {} + fn update_jump_list( + &self, + _menus: Vec, + _entries: Vec>, + ) -> Task>> { + Task::ready(Vec::new()) + } + fn on_app_menu_action(&self, callback: Box); + fn on_will_open_app_menu(&self, callback: Box); + fn on_validate_app_menu_command(&self, callback: Box bool>); + + fn thermal_state(&self) -> ThermalState; + fn on_thermal_state_change(&self, callback: Box); + fn prevent_idle_sleep(&self, reason: &str) -> Task>; + + /// Sets the application's process-wide identity and user-visible name. + /// + /// The identifier is used for platform identity mechanisms such as the + /// Windows AppUserModelID. The name is used wherever the operating system + /// presents the application to the user. Call this once, early in startup, + /// before opening windows or posting notifications. + fn set_app_identity(&self, identifier: &str, name: &str) { + _ = (identifier, name); + } + + /// Posts a notification to the operating system's notification center. + /// + /// Posting a notification whose [`SystemNotification::tag`] matches an + /// earlier one replaces that notification where the platform supports it. + /// No-op on platforms without notification support, or when delivery is + /// unavailable (e.g. authorization was denied). + fn show_system_notification(&self, notification: SystemNotification) { + _ = notification; + } + + /// Removes the delivered or pending notification with this tag. + /// + /// Best-effort: some platforms cannot retract a notification once shown, + /// in which case it ages out of the notification center on its own. + fn dismiss_system_notification(&self, tag: &str) { + _ = tag; + } + + /// Registers the callback invoked when the user activates a system + /// notification, either by clicking its body or one of its action + /// buttons. + /// + /// Implementations must invoke the callback on the main thread. + fn on_system_notification_response( + &self, + callback: Box, + ) { + _ = callback; + } + + fn compositor_name(&self) -> &'static str { + "" + } + /// The environment of the display server a platform that can switch windowing modes is + /// connected to. See [`App::graphical_environment`]. + fn graphical_environment(&self) -> Option { + None + } + fn app_path(&self) -> Result; + fn path_for_auxiliary_executable(&self, name: &str) -> Result; + + fn set_cursor_style(&self, style: CursorStyle); + + /// Hides the mouse cursor until the user moves the mouse over one of + /// this application's windows. + fn hide_cursor_until_mouse_moves(&self); + + /// Returns whether the mouse cursor is currently visible. + fn is_cursor_visible(&self) -> bool; + + fn should_auto_hide_scrollbars(&self) -> bool; + + fn read_from_clipboard(&self) -> Option; + fn write_to_clipboard(&self, item: ClipboardItem); + + /// Reads the clipboard, resolving once its contents are available. + /// + /// Most platforms read synchronously and return a ready task. Platforms + /// whose clipboard access is inherently asynchronous and permission-gated + /// (e.g. the browser's async clipboard API) override this method; on those + /// platforms [`Platform::read_from_clipboard`] cannot return the clipboard + /// contents, so callers that can await should prefer this method. + fn read_from_clipboard_async(&self) -> Task, ClipboardReadError>> { + Task::ready(Ok(self.read_from_clipboard())) + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn read_from_primary(&self) -> Option; + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn write_to_primary(&self, item: ClipboardItem); + + #[cfg(target_os = "macos")] + fn read_from_find_pasteboard(&self) -> Option; + #[cfg(target_os = "macos")] + fn write_to_find_pasteboard(&self, item: ClipboardItem); + + fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; + fn read_credentials(&self, url: &str) -> Task)>>>; + fn delete_credentials(&self, url: &str) -> Task>; + + fn keyboard_layout(&self) -> Box; + fn keyboard_mapper(&self) -> Rc; + fn on_keyboard_layout_change(&self, callback: Box); +} + +/// A handle to a platform's display, e.g. a monitor or laptop screen. +pub trait PlatformDisplay: Debug { + /// Get the ID for this display + fn id(&self) -> DisplayId; + + /// Returns a stable identifier for this display that can be persisted and used + /// across system restarts. + fn uuid(&self) -> Result; + + /// Get the bounds for this display + fn bounds(&self) -> Bounds; + + /// Get the visible bounds for this display, excluding taskbar/dock areas. + /// This is the usable area where windows can be placed without being obscured. + /// Defaults to the full display bounds if not overridden. + fn visible_bounds(&self) -> Bounds { + self.bounds() + } + + /// Get the default bounds for this display to place a window + fn default_bounds(&self) -> Bounds { + let bounds = self.bounds(); + let center = bounds.center(); + let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size); + + let offset = clipped_window_size / 2.0; + let origin = point(center.x - offset.width, center.y - offset.height); + Bounds::new(origin, clipped_window_size) + } +} + +/// A notification posted to the operating system's notification center, +/// rather than rendered as in-app UI. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SystemNotification { + /// Stable identity for the notification. Posting a new notification with + /// the same tag replaces the previous one where the platform supports it, + /// and responses carry the tag back to the application. + pub tag: SharedString, + /// The notification's headline. + pub title: SharedString, + /// Additional text displayed below the title. + pub body: SharedString, + /// Buttons offered on the notification. Platforms that cannot display + /// action buttons show the notification without them. + pub actions: Vec, +} + +/// A button offered on a [`SystemNotification`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SystemNotificationAction { + /// Identifies the action in [`SystemNotificationResponse::action_id`] + /// when the user presses this button. + pub id: SharedString, + /// The button's user-visible label. + pub label: SharedString, +} + +/// The user's activation of a [`SystemNotification`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SystemNotificationResponse { + /// The [`SystemNotification::tag`] of the activated notification. + pub tag: SharedString, + /// The pressed action button's [`SystemNotificationAction::id`], or + /// `None` when the user activated the notification body itself. + pub action_id: Option, +} + +/// Thermal state of the system +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ThermalState { + /// System has no thermal constraints + Nominal, + /// System is slightly constrained, reduce discretionary work + Fair, + /// System is moderately constrained, reduce CPU/GPU intensive work + Serious, + /// System is critically constrained, minimize all resource usage + Critical, +} + +/// Metadata for a given [ScreenCaptureSource] +#[derive(Clone)] +pub struct SourceMetadata { + /// Opaque identifier of this screen. + pub id: u64, + /// Human-readable label for this source. + pub label: Option, + /// Whether this source is the main display. + pub is_main: Option, + /// Video resolution of this source. + pub resolution: Size, +} + +/// A source of on-screen video content that can be captured. +pub trait ScreenCaptureSource { + /// Returns metadata for this source. + fn metadata(&self) -> Result; + + /// Start capture video from this source, invoking the given callback + /// with each frame. + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>>; +} + +/// A video stream captured from a screen. +pub trait ScreenCaptureStream { + /// Returns metadata for this source. + fn metadata(&self) -> Result; +} + +/// A frame of video captured from a screen. +pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); + +/// An opaque identifier for a hardware display +#[derive(PartialEq, Eq, Hash, Copy, Clone)] +pub struct DisplayId(pub(crate) u64); + +impl DisplayId { + /// Create a new `DisplayId` from a raw platform display identifier. + pub fn new(id: u64) -> Self { + Self(id) + } +} + +impl From for DisplayId { + fn from(id: u64) -> Self { + Self(id) + } +} + +impl From for u64 { + fn from(id: DisplayId) -> Self { + id.0 + } +} + +impl Debug for DisplayId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DisplayId({})", self.0) + } +} + +/// Which part of the window to resize +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeEdge { + /// The top edge + Top, + /// The top right corner + TopRight, + /// The right edge + Right, + /// The bottom right corner + BottomRight, + /// The bottom edge + Bottom, + /// The bottom left corner + BottomLeft, + /// The left edge + Left, + /// The top left corner + TopLeft, +} + +/// A type to describe the appearance of a window +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum WindowDecorations { + #[default] + /// Server side decorations + Server, + /// Client side decorations + Client, +} + +/// A type to describe how this window is currently configured +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum Decorations { + /// The window is configured to use server side decorations + #[default] + Server, + /// The window is configured to use client side decorations + Client { + /// The edge tiling state + tiling: Tiling, + }, +} + +/// What window controls this platform supports +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct WindowControls { + /// Whether this platform supports fullscreen + pub fullscreen: bool, + /// Whether this platform supports maximize + pub maximize: bool, + /// Whether this platform supports minimize + pub minimize: bool, + /// Whether this platform supports a window menu + pub window_menu: bool, +} + +impl Default for WindowControls { + fn default() -> Self { + // Assume that we can do anything, unless told otherwise + Self { + fullscreen: true, + maximize: true, + minimize: true, + window_menu: true, + } + } +} + +/// A window control button type used in [`WindowButtonLayout`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum WindowButton { + /// The minimize button + Minimize, + /// The maximize button + Maximize, + /// The close button + Close, +} + +impl WindowButton { + /// Returns a stable element ID for rendering this button. + pub fn id(&self) -> &'static str { + match self { + WindowButton::Minimize => "minimize", + WindowButton::Maximize => "maximize", + WindowButton::Close => "close", + } + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn index(&self) -> usize { + match self { + WindowButton::Minimize => 0, + WindowButton::Maximize => 1, + WindowButton::Close => 2, + } + } +} + +/// Maximum number of [`WindowButton`]s per side in the titlebar. +pub const MAX_BUTTONS_PER_SIDE: usize = 3; + +/// Describes which [`WindowButton`]s appear on each side of the titlebar. +/// +/// On Linux, this is read from the desktop environment's configuration +/// (e.g. GNOME's `gtk-decoration-layout` gsetting) via [`WindowButtonLayout::parse`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WindowButtonLayout { + /// Buttons on the left side of the titlebar. + pub left: [Option; MAX_BUTTONS_PER_SIDE], + /// Buttons on the right side of the titlebar. + pub right: [Option; MAX_BUTTONS_PER_SIDE], +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +impl WindowButtonLayout { + /// Returns Zed's built-in fallback button layout for Linux titlebars. + pub fn linux_default() -> Self { + Self { + left: [None; MAX_BUTTONS_PER_SIDE], + right: [ + Some(WindowButton::Minimize), + Some(WindowButton::Maximize), + Some(WindowButton::Close), + ], + } + } + + /// Parses a GNOME-style `button-layout` string (e.g. `"close,minimize:maximize"`). + pub fn parse(layout_string: &str) -> Result { + fn parse_side( + s: &str, + seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE], + unrecognized: &mut Vec, + ) -> [Option; MAX_BUTTONS_PER_SIDE] { + let mut result = [None; MAX_BUTTONS_PER_SIDE]; + let mut i = 0; + for name in s.split(',') { + let trimmed = name.trim(); + if trimmed.is_empty() { + continue; + } + let button = match trimmed { + "minimize" => Some(WindowButton::Minimize), + "maximize" => Some(WindowButton::Maximize), + "close" => Some(WindowButton::Close), + other => { + unrecognized.push(other.to_string()); + None + } + }; + if let Some(button) = button { + if seen_buttons[button.index()] { + continue; + } + if let Some(slot) = result.get_mut(i) { + *slot = Some(button); + seen_buttons[button.index()] = true; + i += 1; + } + } + } + result + } + + let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string)); + let mut unrecognized = Vec::new(); + let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE]; + let layout = Self { + left: parse_side(left_str, &mut seen_buttons, &mut unrecognized), + right: parse_side(right_str, &mut seen_buttons, &mut unrecognized), + }; + + if !unrecognized.is_empty() + && layout.left.iter().all(Option::is_none) + && layout.right.iter().all(Option::is_none) + { + bail!( + "button layout string {:?} contains no valid buttons (unrecognized: {})", + layout_string, + unrecognized.join(", ") + ); + } + + Ok(layout) + } + + /// Formats the layout back into a GNOME-style `button-layout` string. + #[cfg(test)] + pub fn format(&self) -> String { + fn format_side(buttons: &[Option; MAX_BUTTONS_PER_SIDE]) -> String { + buttons + .iter() + .flatten() + .map(|button| match button { + WindowButton::Minimize => "minimize", + WindowButton::Maximize => "maximize", + WindowButton::Close => "close", + }) + .collect::>() + .join(",") + } + + format!("{}:{}", format_side(&self.left), format_side(&self.right)) + } +} + +/// A type to describe which sides of the window are currently tiled in some way +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub struct Tiling { + /// Whether the top edge is tiled + pub top: bool, + /// Whether the left edge is tiled + pub left: bool, + /// Whether the right edge is tiled + pub right: bool, + /// Whether the bottom edge is tiled + pub bottom: bool, +} + +impl Tiling { + /// Initializes a [`Tiling`] type with all sides tiled + pub fn tiled() -> Self { + Self { + top: true, + left: true, + right: true, + bottom: true, + } + } + + /// Whether any edge is tiled + pub fn is_tiled(&self) -> bool { + self.top || self.left || self.right || self.bottom + } +} + +/// Callbacks for the accessibility adapter. +pub struct A11yCallbacks { + /// Called when the adapter is activated (a screen reader connects). + pub activation: Box Option + Send + 'static>, + /// Called when an action is requested by the screen reader. + pub action: Box, + /// Called when the adapter is deactivated (screen reader disconnects). + pub deactivation: Box, +} + +/// The source of a platform frame request's timestamp. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +pub enum FrameRequestSource { + /// An OS callback or compositor-paced frame request. + #[default] + NativeCallback, + /// A refresh timer, retry, queued wakeup, or fallback sleep paced by the app. + LocalSchedule, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] +#[expect(missing_docs)] +pub struct RequestFrameOptions { + /// Whether a presentation is required. + pub require_presentation: bool, + /// Force refresh of all rendering states when true. + pub force_render: bool, + /// When the platform first requested this frame, before main-thread dispatch. + /// + /// `None` means the captured request time is unavailable. + /// Coalesced requests carry their first request time, not their delivery time. + /// Built-in backends collect timestamps only with the `profiler` feature. + pub signal_at: Option, + /// Distinguishes native callbacks from local scheduling requests. + pub signal_source: FrameRequestSource, +} + +/// Preserves the first platform frame request time across coalesced notifications. +/// +/// Producers may record from a platform thread without waiting for the UI thread. +/// The consumer drains the timestamp before dispatching the request on the UI thread. +/// The timestamp offset and source share one atomic word so coalescing cannot +/// mix a request's time with another request's source. The low bit encodes the +/// source; the remaining bits encode nanoseconds from `origin`. `u64::MAX` is empty. +/// Without the `profiler` feature, this accumulator has no timestamp storage. +pub struct PlatformFrameSignal { + #[cfg(feature = "profiler")] + origin: Instant, + #[cfg(feature = "profiler")] + first_signal: std::sync::atomic::AtomicU64, +} + +impl Default for PlatformFrameSignal { + fn default() -> Self { + Self::new() + } +} + +impl PlatformFrameSignal { + /// Creates an empty signal accumulator. + pub fn new() -> Self { + Self { + #[cfg(feature = "profiler")] + origin: Instant::now(), + #[cfg(feature = "profiler")] + first_signal: std::sync::atomic::AtomicU64::new(u64::MAX), + } + } + + /// Captures a platform request timestamp only when profiling is enabled. + /// + /// The closure is not invoked in profiler-disabled builds. + #[inline] + pub fn capture(capture: impl FnOnce() -> Instant) -> Option { + if cfg!(feature = "profiler") { + Some(capture()) + } else { + None + } + } + + /// Records a platform frame request, retaining the first undrained timestamp. + #[inline] + #[cfg_attr(not(feature = "profiler"), expect(unused_variables))] + pub fn record(&self, at: Instant, source: FrameRequestSource) { + #[cfg(feature = "profiler")] + { + let nanoseconds = at + .saturating_duration_since(self.origin) + .as_nanos() + .min(u128::from((u64::MAX >> 1) - 1)) as u64; + let source_bit = match source { + FrameRequestSource::NativeCallback => 0, + FrameRequestSource::LocalSchedule => 1, + }; + self.first_signal.fetch_min( + (nanoseconds << 1) | source_bit, + std::sync::atomic::Ordering::Relaxed, + ); + } + } + + /// Drains the first platform frame request time and source, leaving the accumulator empty. + #[inline] + pub fn take(&self) -> Option<(Instant, FrameRequestSource)> { + #[cfg(feature = "profiler")] + { + let signal = self + .first_signal + .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed); + (signal != u64::MAX).then(|| { + let source = match signal & 1 { + 0 => FrameRequestSource::NativeCallback, + _ => FrameRequestSource::LocalSchedule, + }; + (self.origin + Duration::from_nanos(signal >> 1), source) + }) + } + #[cfg(not(feature = "profiler"))] + { + None + } + } +} + +/// The application's lifecycle phase, as owned and reported by a mobile OS. +/// +/// `Inactive` means visible but not receiving input (a system dialog on +/// top), while `Background` means not visible at all, with process death +/// possible at any time thereafter. +/// +/// | Phase | iOS | Android | +/// |--------------|------------------------------|--------------| +/// | `Active` | `didBecomeActive` | `onResume` | +/// | `Inactive` | `willResignActive` | `onPause` | +/// | `Background` | `didEnterBackground` | `onStop` | +/// | `Foreground` | `willEnterForeground` | `onStart` | +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum AppLifecyclePhase { + /// Foreground and receiving input. + Active, + /// Foreground (visible) but not receiving input. + Inactive, + /// Not visible. The GPU surface may be destroyed while backgrounded and + /// the process may be killed without further notice. + Background, + /// Becoming visible again, before input is restored. + Foreground, +} + +/// Regions of a window that are obscured or reserved by the system. +/// +/// Mobile applications often share space in their window with system-specific +/// geometry, from keyboards to camera notches. In GPUI, all this is abstracted +/// into a single "inset" which should be overlaid on the window's bounds. +/// It is up to the application develop to determine how to handle these cases. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct WindowInsets { + /// Regions covered by system UI or hardware: status bar, display + /// cutouts/notch, home indicator, navigation bars. + /// (iOS: `safeAreaInsets`. Android: `WindowInsets` of types + /// `systemBars() | displayCutout()`.) + pub safe_area: Edges, + /// The region covered by the keyboard, when present. + /// (iOS: derived from `keyboardWillShow`/frame-change notifications. + /// Android: `WindowInsets.Type.ime()`.) + pub ime: Edges, +} + +impl WindowInsets { + /// The combined inset content should avoid. + pub fn effective(&self) -> Edges { + Edges { + top: self.safe_area.top.max(self.ime.top), + right: self.safe_area.right.max(self.ime.right), + bottom: self.safe_area.bottom.max(self.ime.bottom), + left: self.safe_area.left.max(self.ime.left), + } + } +} + +/// A change in the state of the focused text input. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TextInputStateChange { + /// The window changed from having no active text input to having one. + FocusGained, + /// The window no longer has an active text input. + FocusLost, + /// The selection or caret moved + SelectionChanged, + /// The document content changed outside of platform-initiated edits. + ContentChanged, +} + +#[expect(missing_docs)] +pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { + fn bounds(&self) -> Bounds; + fn is_maximized(&self) -> bool; + fn window_bounds(&self) -> WindowBounds; + fn content_size(&self) -> Size; + /// Returns the visible viewport in logical pixels relative to the content origin. + /// + /// This may be smaller or offset when a keyboard or zoom obscures content; + /// it must not change the full layout size returned by `content_size`. + /// Implementations should return a frame snapshot, not query platform layout here. + fn visual_viewport_bounds(&self) -> Bounds { + Bounds::new(Point::default(), self.content_size()) + } + /// Registers a callback when visible geometry may have changed. + /// + /// This requests a frame; backends can sample the new viewport and safe-area + /// geometry in `prepare_frame` rather than updating it inside the callback. + fn on_visual_viewport_changed(&self, _callback: Box) {} + /// Samples platform geometry before a draw, returning whether view caches must be invalidated. + /// + /// Geometry getters must remain consistent throughout the ensuing draw. + /// Do not invoke callbacks here: GPUI is already updating this window. + fn prepare_frame(&self) -> bool { + false + } + fn resize(&mut self, size: Size); + fn scale_factor(&self) -> f32; + fn appearance(&self) -> WindowAppearance; + fn display(&self) -> Option>; + fn mouse_position(&self) -> Point; + fn modifiers(&self) -> Modifiers; + fn capslock(&self) -> Capslock; + fn set_input_handler(&mut self, input_handler: PlatformInputHandler); + fn take_input_handler(&mut self) -> Option; + /// Apply the focused text region's [`TextInputConfiguration`] to the + /// platform's text input session (e.g. attributes of the hidden editable + /// element on web). Called only when the configuration changes, because + /// reconfiguring a live input session can restart the IME connection. + fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {} + fn prompt( + &self, + level: PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option>; + fn activate(&self); + /// Requests that the operating system draw attention to this window. + fn request_attention(&self) {} + fn is_active(&self) -> bool; + /// The current [`WindowVisibility`]. Read once when the window is created; + /// afterwards changes arrive through [`Self::on_visibility_change`]. + fn visibility(&self) -> WindowVisibility; + fn is_hovered(&self) -> bool; + fn background_appearance(&self) -> WindowBackgroundAppearance; + fn set_title(&mut self, title: &str); + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance); + fn minimize(&self); + fn zoom(&self); + fn toggle_fullscreen(&self); + fn is_fullscreen(&self) -> bool; + fn frame_waker(&self) -> Option> { + None + } + fn on_request_frame(&self, callback: Box); + fn on_input(&self, callback: Box DispatchEventResult>); + fn on_active_status_change(&self, callback: Box); + /// Registers the callback invoked when [`Self::visibility`] changes. Only + /// transitions are reported; the callback runs on the main thread outside + /// of any window update. + fn on_visibility_change(&self, callback: Box); + fn on_hover_status_change(&self, callback: Box); + fn on_resize(&self, callback: Box, f32)>); + fn on_moved(&self, callback: Box); + fn on_should_close(&self, callback: Box bool>); + fn on_hit_test_window_control(&self, callback: Box Option>); + fn on_close(&self, callback: Box); + fn on_appearance_changed(&self, callback: Box); + fn on_button_layout_changed(&self, _callback: Box) {} + fn draw(&self, scene: &Scene); + fn schedule_frame(&self) {} + fn sprite_atlas(&self) -> Arc; + fn is_subpixel_rendering_supported(&self) -> bool; + + // macOS specific methods + fn get_title(&self) -> String { + String::new() + } + fn tabbed_windows(&self) -> Option> { + None + } + fn tab_bar_visible(&self) -> bool { + false + } + fn set_edited(&mut self, _edited: bool) {} + fn set_document_path(&self, _path: Option<&std::path::Path>) {} + fn toggle_simple_fullscreen(&self) {} + fn is_simple_fullscreen(&self) -> bool { + false + } + #[cfg(target_os = "macos")] + fn set_traffic_light_position(&self, _position: Point) {} + fn show_character_palette(&self) {} + fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {} + fn on_move_tab_to_new_window(&self, _callback: Box) {} + fn on_merge_all_windows(&self, _callback: Box) {} + fn on_select_previous_tab(&self, _callback: Box) {} + fn on_select_next_tab(&self, _callback: Box) {} + fn on_toggle_tab_bar(&self, _callback: Box) {} + fn merge_all_windows(&self) {} + fn move_tab_to_new_window(&self) {} + fn toggle_window_tab_overview(&self) {} + fn set_tabbing_identifier(&self, _identifier: Option) {} + + fn native_window_state(&self) -> Option> { + None + } + fn restore_native_window_state(&self, _state: &[u8]) {} + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND; + + // Linux specific methods + fn inner_window_bounds(&self) -> WindowBounds { + self.window_bounds() + } + fn request_decorations(&self, _decorations: WindowDecorations) {} + fn show_window_menu(&self, _position: Point) {} + fn start_window_move(&self) {} + fn can_start_external_drag(&self) -> bool { + false + } + fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool { + false + } + fn start_window_resize(&self, _edge: ResizeEdge) {} + fn set_exclusive_zone(&self, _zone: Pixels) {} + #[cfg(all(target_os = "linux", feature = "wayland"))] + fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {} + fn set_input_region(&self, _region: Option<&[Bounds]>) {} + fn window_decorations(&self) -> Decorations { + Decorations::Server + } + fn set_app_id(&mut self, _app_id: &str) {} + fn map_window(&mut self) -> anyhow::Result<()> { + Ok(()) + } + fn window_controls(&self) -> WindowControls { + WindowControls::default() + } + fn set_client_inset(&self, _inset: Pixels) {} + fn gpu_specs(&self) -> Option; + + fn update_ime_position(&self, _bounds: Bounds); + + // Mobile platform methods. + + /// The regions of this window currently obscured or reserved by the + /// system. Zero on platforms without such regions. + fn insets(&self) -> WindowInsets { + WindowInsets::default() + } + + /// Registers a callback invoked whenever [`Self::insets`] change. + /// + /// Contract: fires continuously during animated transitions (Android + /// `WindowInsetsAnimation` progress; on iOS the platform interpolates + /// the keyboard animation curve on frame ticks) and is exact at rest. + fn on_insets_changed(&self, _callback: Box) {} + + /// Sets the handler for the system back action (Android back + /// button/gesture; no source on iOS or desktop). + fn set_back_handler(&self, _callback: Box) {} + + /// Declares whether the application would currently handle the system + /// back action (e.g. navigation depth > 0). + fn set_back_enabled(&self, _enabled: bool) {} + + /// Requests that the soft keyboard be shown. + fn show_soft_keyboard(&self) {} + + /// Requests that the soft keyboard be hidden. + fn hide_soft_keyboard(&self) {} + + /// Inform the operating system that the text input state has changed + fn text_input_state_changed(&self, _change: TextInputStateChange) {} + + fn play_system_bell(&self) {} + + /// Initialize the accessibility adapter with callbacks. + fn a11y_init(&self, _callbacks: A11yCallbacks) {} + + /// Provide a TreeUpdate to the accessibility adapter. + fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {} + + /// Inform the adapter of updated window bounds. + fn a11y_update_window_bounds(&self) {} + + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn as_test(&mut self) -> Option<&mut TestWindow> { + None + } + + /// Renders the given scene to a texture and returns the pixel data as an RGBA image. + /// This does not present the frame to screen - useful for visual testing where we want + /// to capture what would be rendered without displaying it or requiring the window to be visible. + #[cfg(any(test, feature = "test-support"))] + fn render_to_image(&self, _scene: &Scene) -> Result { + anyhow::bail!("render_to_image not implemented for this platform") + } +} + +/// A renderer for headless windows that can produce real rendered output. +#[cfg(any(test, feature = "test-support", feature = "bench-support"))] +pub trait PlatformHeadlessRenderer { + /// Render a scene and return the result as an RGBA image. + fn render_scene_to_image( + &mut self, + scene: &Scene, + size: Size, + ) -> Result; + + /// Render a scene to an offscreen target without reading the result back. + /// + /// This is the headless analogue of presenting a frame: it performs the + /// same CPU-side scene encoding and GPU submission as drawing to a real + /// window, but doesn't block on GPU completion or copy pixels back. + fn render_scene(&mut self, scene: &Scene, size: Size) -> Result<()>; + + /// Returns the sprite atlas used by this renderer. + fn sprite_atlas(&self) -> Arc; +} + +/// Type alias for runnables with metadata. +/// Previously an enum with a single variant, now simplified to a direct type alias. +#[doc(hidden)] +pub type RunnableVariant = Runnable; + +#[doc(hidden)] +pub type TimerResolutionGuard = gpui_util::Deferred>; + +#[doc(hidden)] +pub enum TasksIncluded { + OnlyCompleted, + CompletedAndRunning, +} + +/// This type is public so that our test macro can generate and use it, but it should not +/// be considered part of our public API. +#[doc(hidden)] +pub trait PlatformDispatcher: Send + Sync { + fn is_main_thread(&self) -> bool; + fn dispatch(&self, runnable: RunnableVariant, priority: Priority); + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority); + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant); + + fn dispatch_on_main_thread_when_idle( + &self, + runnable: RunnableVariant, + timeout: Option, + ) { + let _ = timeout; + self.dispatch_on_main_thread(runnable, Priority::Low); + } + + fn idle_time_remaining(&self) -> Option { + None + } + + fn spawn_realtime(&self, f: Box); + + fn now(&self) -> Instant { + Instant::now() + } + + fn increase_timer_resolution(&self) -> TimerResolutionGuard { + gpui_util::defer(Box::new(|| {})) + } + + fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard { + ActivityGuard::noop() + } + + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn as_test(&self) -> Option<&TestDispatcher> { + None + } + + // This cfg must match the `threaded_dispatcher` module's, which implements + // this method whenever it compiles. + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn as_threaded(&self) -> Option<&ThreadedDispatcher> { + None + } +} + +#[expect(missing_docs)] +pub trait PlatformTextSystem: Send + Sync { + fn add_fonts(&self, fonts: Vec>) -> Result<()>; + /// Installs a nonblocking sink for unresolved grapheme clusters. + fn set_missing_glyph_sink(&self, _sink: Option>) {} + /// Get all available font names. + fn all_font_names(&self) -> Vec; + /// Get the font ID for a font descriptor. + fn font_id(&self, descriptor: &Font) -> Result; + /// Prewarm any system font caches needed to shape text. + fn prewarm_fonts(&self, _font_ids: &[FontId]) {} + /// Get metrics for a font. + fn font_metrics(&self, font_id: FontId) -> FontMetrics; + /// Get typographic bounds for a glyph. + fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + /// Get the advance width for a glyph. + fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; + /// Get the glyph ID for a character. + fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; + /// Get raster bounds for a glyph. + fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; + /// Rasterize a glyph. + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; + /// Layout a line of text with the given font runs. + fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; + /// Returns the recommended text rendering mode for the given font and size. + fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels) + -> TextRenderingMode; + /// Returns the dilation level to use for a glyph painted in the given color. + fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 { + 0 + } +} + +#[expect(missing_docs)] +pub struct NoopTextSystem; + +#[expect(missing_docs)] +impl NoopTextSystem { + #[allow(dead_code)] + pub fn new() -> Self { + Self + } +} + +impl PlatformTextSystem for NoopTextSystem { + fn add_fonts(&self, _fonts: Vec>) -> Result<()> { + Ok(()) + } + + fn all_font_names(&self) -> Vec { + Vec::new() + } + + fn font_id(&self, _descriptor: &Font) -> Result { + Ok(FontId(1)) + } + + fn font_metrics(&self, _font_id: FontId) -> FontMetrics { + FontMetrics { + units_per_em: 1000, + ascent: 1025.0, + descent: -275.0, + line_gap: 0.0, + underline_position: -95.0, + underline_thickness: 60.0, + cap_height: 698.0, + x_height: 516.0, + bounding_box: Bounds { + origin: Point { + x: -260.0, + y: -245.0, + }, + size: Size { + width: 1501.0, + height: 1364.0, + }, + }, + } + } + + fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result> { + Ok(Bounds { + origin: Point { x: 54.0, y: 0.0 }, + size: size(392.0, 528.0), + }) + } + + fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result> { + Ok(size(600.0 * glyph_id.0 as f32, 0.0)) + } + + fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option { + Some(GlyphId(ch.len_utf16() as u32)) + } + + fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result> { + Ok(Default::default()) + } + + fn rasterize_glyph( + &self, + _params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)> { + Ok((raster_bounds.size, Vec::new())) + } + + fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout { + let mut position = px(0.); + let metrics = self.font_metrics(FontId(0)); + let em_width = font_size + * self + .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap()) + .unwrap() + .width + / metrics.units_per_em as f32; + let mut glyphs = Vec::new(); + for (ix, c) in text.char_indices() { + if let Some(glyph) = self.glyph_for_char(FontId(0), c) { + glyphs.push(ShapedGlyph { + id: glyph, + position: point(position, px(0.)), + index: ix, + is_emoji: glyph.0 == 2, + }); + if glyph.0 == 2 { + position += em_width * 2.0; + } else { + position += em_width; + } + } else { + position += em_width + } + } + let mut runs = Vec::default(); + if !glyphs.is_empty() { + runs.push(ShapedRun { + font_id: FontId(0), + glyphs, + }); + } else { + position = px(0.); + } + + LineLayout { + font_size, + width: position, + ascent: font_size * (metrics.ascent / metrics.units_per_em as f32), + descent: font_size * (metrics.descent / metrics.units_per_em as f32), + runs, + len: text.len(), + } + } + + fn recommended_rendering_mode( + &self, + _font_id: FontId, + _font_size: Pixels, + ) -> TextRenderingMode { + TextRenderingMode::Grayscale + } +} + +// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/// Compute gamma correction ratios for subpixel text rendering. +#[allow(dead_code)] +pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] { + const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ + [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 + [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 + [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 + [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 + [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 + [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 + [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 + [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 + [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 + [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 + [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 + [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 + [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 + ]; + + const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; + const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; + + let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; + let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; + + [ + ratios[0] * NORM13, + ratios[1] * NORM24, + ratios[2] * NORM13, + ratios[3] * NORM24, + ] +} + +#[derive(PartialEq, Eq, Hash, Clone)] +#[expect(missing_docs)] +pub enum AtlasKey { + Glyph(RenderGlyphParams), + Svg(RenderSvgParams), + Image(RenderImageParams), +} + +impl AtlasKey { + /// Returns the texture kind for this atlas key. + pub fn texture_kind(&self) -> AtlasTextureKind { + match self { + AtlasKey::Glyph(params) => { + if params.is_emoji { + AtlasTextureKind::Polychrome + } else if params.subpixel_rendering { + AtlasTextureKind::Subpixel + } else { + AtlasTextureKind::Monochrome + } + } + AtlasKey::Svg(_) => AtlasTextureKind::Monochrome, + AtlasKey::Image(_) => AtlasTextureKind::Polychrome, + } + } +} + +impl From for AtlasKey { + fn from(params: RenderGlyphParams) -> Self { + Self::Glyph(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderSvgParams) -> Self { + Self::Svg(params) + } +} + +impl From for AtlasKey { + fn from(params: RenderImageParams) -> Self { + Self::Image(params) + } +} + +#[expect(missing_docs)] +pub trait PlatformAtlas { + /// The builder runs with the atlas locked and must not re-enter the same atlas. + fn get_or_insert_with<'a>( + &self, + key: AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result>; + fn remove(&self, key: &AtlasKey); + + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn contains(&self, _key: &AtlasKey) -> bool { + false + } +} + +#[doc(hidden)] +pub trait AtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + bytes: &[u8], + ) -> Result; + + fn remove(&mut self, tile: AtlasTile); +} + +#[doc(hidden)] +pub struct AtlasState { + tiles_by_key: FxHashMap, + pub backend: Backend, +} + +impl AtlasState { + pub fn new(backend: Backend) -> Self { + Self { + tiles_by_key: FxHashMap::default(), + backend, + } + } + + pub fn contains(&self, key: &AtlasKey) -> bool { + self.tiles_by_key.contains_key(key) + } + + pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) { + self.tiles_by_key.clear(); + reset_backend(&mut self.backend); + } +} + +impl Default for AtlasState { + fn default() -> Self { + Self::new(Backend::default()) + } +} + +impl AtlasState { + pub fn get_or_insert_with<'a>( + &mut self, + key: AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + match self.tiles_by_key.entry(key) { + Entry::Occupied(entry) => Ok(Some(*entry.get())), + Entry::Vacant(entry) => { + profiling::scope!("new tile"); + let Some((size, bytes)) = build()? else { + return Ok(None); + }; + let tile = self + .backend + .insert(entry.key().texture_kind(), size, &bytes)?; + entry.insert(tile); + Ok(Some(tile)) + } + } + } + + pub fn remove(&mut self, key: &AtlasKey) { + if let Some(tile) = self.tiles_by_key.remove(key) { + self.backend.remove(tile); + } + } +} + +/// A sprite atlas for windows without a GPU. It hands out uniquely identified +/// tiles without uploading any pixels, so glyph, SVG, and image painting can +/// run to completion in tests and headless platforms. +#[derive(Default)] +pub struct HeadlessAtlas(parking_lot::Mutex>); + +#[doc(hidden)] +#[derive(Default)] +pub struct HeadlessAtlasBackend { + next_id: u32, +} + +impl AtlasBackend for HeadlessAtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + _bytes: &[u8], + ) -> Result { + self.next_id += 1; + let texture_id = self.next_id; + self.next_id += 1; + let tile_id = self.next_id; + Ok(AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind, + }, + tile_id: TileId(tile_id), + padding: 0, + bounds: Bounds { + origin: Point::default(), + size, + }, + }) + } + + fn remove(&mut self, _tile: AtlasTile) {} +} + +impl PlatformAtlas for HeadlessAtlas { + fn get_or_insert_with<'a>( + &self, + key: AtlasKey, + build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, + ) -> Result> { + self.0.lock().get_or_insert_with(key, build) + } + + fn remove(&self, key: &AtlasKey) { + self.0.lock().remove(key); + } + + #[cfg(any(test, feature = "test-support", feature = "bench-support"))] + fn contains(&self, key: &AtlasKey) -> bool { + self.0.lock().contains(key) + } +} + +#[doc(hidden)] +pub struct AtlasTextureList { + pub textures: Vec>, + pub free_list: Vec, +} + +impl Default for AtlasTextureList { + fn default() -> Self { + Self { + textures: Vec::default(), + free_list: Vec::default(), + } + } +} + +impl ops::Index for AtlasTextureList { + type Output = Option; + + fn index(&self, index: usize) -> &Self::Output { + &self.textures[index] + } +} + +impl AtlasTextureList { + #[allow(unused)] + pub fn drain(&mut self) -> std::vec::Drain<'_, Option> { + self.free_list.clear(); + self.textures.drain(..) + } + + #[allow(dead_code)] + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.textures.iter_mut().flatten() + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(C)] +#[expect(missing_docs)] +pub struct AtlasTile { + /// The texture this tile belongs to. + pub texture_id: AtlasTextureId, + /// The unique ID of this tile within its texture. + pub tile_id: TileId, + /// Padding around the tile content in pixels. + pub padding: u32, + /// The bounds of this tile within the texture. + pub bounds: Bounds, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +#[expect(missing_docs)] +pub struct AtlasTextureId { + // We use u32 instead of usize for Metal Shader Language compatibility + /// The index of this texture in the atlas. + pub index: u32, + /// The kind of content stored in this texture. + pub kind: AtlasTextureKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +#[expect(missing_docs)] +pub enum AtlasTextureKind { + Monochrome = 0, + Polychrome = 1, + Subpixel = 2, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(C)] +#[expect(missing_docs)] +pub struct TileId(pub u32); + +impl From for TileId { + fn from(id: etagere::AllocId) -> Self { + Self(id.serialize()) + } +} + +impl From for etagere::AllocId { + fn from(id: TileId) -> Self { + Self::deserialize(id.0) + } +} + +#[expect(missing_docs)] +pub struct PlatformInputHandler { + cx: AsyncWindowContext, + handler: Box, +} + +#[expect(missing_docs)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +impl PlatformInputHandler { + pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { + Self { cx, handler } + } + + pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option { + self.cx + .update(|window, cx| { + self.handler + .selected_text_range(ignore_disabled_input, window, cx) + }) + .ok() + .flatten() + } + + #[cfg_attr(target_os = "windows", allow(dead_code))] + pub fn marked_text_range(&mut self) -> Option> { + self.cx + .update(|window, cx| self.handler.marked_text_range(window, cx)) + .ok() + .flatten() + } + + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + pub fn text_for_range( + &mut self, + range_utf16: Range, + adjusted: &mut Option>, + ) -> Option { + self.cx + .update(|window, cx| { + self.handler + .text_for_range(range_utf16, adjusted, window, cx) + }) + .ok() + .flatten() + } + + pub fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.cx + .update(|window, cx| { + self.handler + .replace_text_in_range(replacement_range, text, window, cx); + }) + .ok(); + } + + pub fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.cx + .update(|window, cx| { + self.handler.replace_and_mark_text_in_range( + range_utf16, + new_text, + new_selected_range, + window, + cx, + ) + }) + .ok(); + } + + #[cfg_attr(target_os = "windows", allow(dead_code))] + pub fn unmark_text(&mut self) { + self.cx + .update(|window, cx| self.handler.unmark_text(window, cx)) + .ok(); + } + + pub fn paste(&mut self, item: ClipboardItem) { + self.cx + .update(|window, cx| self.handler.paste(item, window, cx)) + .ok(); + } + + pub fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { + self.cx + .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx)) + .ok() + .flatten() + } + + #[allow(dead_code)] + pub fn apple_press_and_hold_enabled(&mut self) -> bool { + self.handler.apple_press_and_hold_enabled() + } + + pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) { + self.handler.replace_text_in_range(None, input, window, cx); + } + + pub fn compute_ime_candidate_bounds( + marked_range: Option>, + selection: &UTF16Selection, + mut bounds_for_range: impl FnMut(Range) -> Option>, + ) -> Option> { + if let Some(marked_range) = marked_range { + // Default to the start of the marked (composing) range. + let mut line_start = marked_range.start; + + // Walk backward from the caret looking for a line break. A change in + // the Y coordinate means we crossed into the previous visual line, so + // the line start is one position after the break point. + let caret = selection.range.end; + if let Some(caret_bounds) = bounds_for_range(caret..caret) { + for i in (marked_range.start..caret).rev() { + if let Some(b) = bounds_for_range(i..i) { + if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) { + line_start = i + 1; + break; + } + } + } + } + bounds_for_range(line_start..line_start) + } else { + // No active composition — use the selection endpoint. + let offset = if selection.reversed { + selection.range.start + } else { + selection.range.end + }; + bounds_for_range(offset..offset) + } + } + + pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option> { + let marked_range = self.handler.marked_text_range(window, cx); + let selection = self.handler.selected_text_range(true, window, cx)?; + Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { + self.handler.bounds_for_range(range, window, cx) + }) + } + + pub fn ime_candidate_bounds(&mut self) -> Option> { + let marked_range = self.marked_text_range(); + let selection = self.selected_text_range(true)?; + Self::compute_ime_candidate_bounds(marked_range, &selection, |range| { + self.bounds_for_range(range) + }) + } + + #[allow(unused)] + pub fn character_index_for_point(&mut self, point: Point) -> Option { + self.cx + .update(|window, cx| self.handler.character_index_for_point(point, window, cx)) + .ok() + .flatten() + } + + /// See [`InputHandler::set_selected_text_range`]. + pub fn set_selected_text_range(&mut self, range_utf16: Range) { + self.cx + .update(|window, cx| { + self.handler + .set_selected_text_range(range_utf16, window, cx) + }) + .ok(); + } + + /// See [`InputHandler::element_bounds`]. + pub fn element_bounds(&mut self) -> Option> { + self.cx + .update(|window, cx| self.handler.element_bounds(window, cx)) + .ok() + .flatten() + } + + /// See [`InputHandler::text_length_utf16`]. + pub fn text_length_utf16(&mut self) -> Option { + self.cx + .update(|window, cx| self.handler.text_length_utf16(window, cx)) + .ok() + .flatten() + } + + #[allow(dead_code)] + pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool { + self.handler.accepts_text_input(window, cx) + } + + #[allow(dead_code)] + pub fn query_accepts_text_input(&mut self) -> bool { + self.cx + .update(|window, cx| self.handler.accepts_text_input(window, cx)) + .unwrap_or(true) + } + + /// See [`InputHandler::prefers_ime_for_printable_keys`]. + /// + /// This is not a pure delegation to the handler: while a multi-stroke binding is pending this + /// returns `false` regardless of the handler's preference, because the next printable key may + /// complete a binding whose prefix already bypassed the IME. + pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool { + self.cx + .update(|window, cx| { + // The next printable key may complete a chord whose prefix bypassed the IME. + !window.has_pending_keystrokes() + && self.handler.prefers_ime_for_printable_keys(window, cx) + }) + .unwrap_or(false) + } + + /// See [`InputHandler::text_input_configuration`]. + pub fn text_input_configuration( + &mut self, + window: &mut Window, + cx: &mut App, + ) -> TextInputConfiguration { + self.handler.text_input_configuration(window, cx) + } + + /// See [`InputHandler::text_input_editable_range`]. + pub fn text_input_editable_range(&mut self) -> Option> { + self.cx + .update(|window, cx| self.handler.text_input_editable_range(window, cx)) + .ok() + .flatten() + } +} + +/// A struct representing a selection in a text buffer, in UTF16 characters. +/// This is different from a range because the head may be before the tail. +#[derive(Debug)] +pub struct UTF16Selection { + /// The range of text in the document this selection corresponds to + /// in UTF16 characters. + pub range: Range, + /// Whether the head of this selection is at the start (true), or end (false) + /// of the range + pub reversed: bool, +} + +/// Zed's interface for handling text input from the platform's IME system +/// This is currently a 1:1 exposure of the NSTextInputClient API: +/// +/// +pub trait InputHandler: 'static { + /// Get the range of the user's currently selected text, if any + /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Get the range of the currently marked text, if any + /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange) + /// + /// Return value is in terms of UTF-16 characters, from 0 to the length of the document + fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option>; + + /// Get the text for the given document range in UTF-16 characters + /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring) + /// + /// range_utf16 is in terms of UTF-16 characters + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Replace the text in the given document range with the given text + /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext) + /// + /// replacement_range is in terms of UTF-16 characters + fn replace_text_in_range( + &mut self, + replacement_range: Option>, + text: &str, + window: &mut Window, + cx: &mut App, + ); + + /// Replace the text in the given document range with the given text, + /// and mark the given text as part of an IME 'composing' state + /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext) + /// + /// range_utf16 is in terms of UTF-16 characters + /// new_selected_range is in terms of UTF-16 characters + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + window: &mut Window, + cx: &mut App, + ); + + /// Remove the IME 'composing' state from the document + /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext) + fn unmark_text(&mut self, window: &mut Window, cx: &mut App); + + /// Insert a platform-initiated paste at the current selection. + /// + /// Platforms that deliver paste as an input event rather than through an + /// application-defined action (e.g. the DOM `paste` event on web) call + /// this with the full clipboard contents. The default implementation + /// inserts only the plain-text portion of the item. + fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) { + if let Some(text) = item.text() { + self.replace_text_in_range(None, &text, window, cx); + } + } + + /// Get the bounds of the given document range in screen coordinates + /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect) + /// + /// This is used for positioning the IME candidate window + fn bounds_for_range( + &mut self, + range_utf16: Range, + window: &mut Window, + cx: &mut App, + ) -> Option>; + + /// Get the character offset for the given point in terms of UTF16 characters + /// + /// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:)) + fn character_index_for_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut App, + ) -> Option; + + /// Set the range of the user's currently selected text. + /// + /// This is the reverse data-flow direction from [`Self::selected_text_range`]: + /// platforms call it when the system text machinery moves the selection on the + /// application's behalf — e.g. the user drags a system selection handle or + /// invokes Select All from system UI (iOS `UITextInput setSelectedTextRange:`, + /// Android `InputConnection.setSelection`). + /// + /// range_utf16 is in terms of UTF-16 characters, from 0 to the length of the document + fn set_selected_text_range( + &mut self, + _range_utf16: Range, + _window: &mut Window, + _cx: &mut App, + ) { + } + + /// Get the bounds of the focused text element in window coordinates, if known. + /// + /// This is the pull counterpart to the [`PlatformWindow::update_ime_position`] + /// push: mobile platforms ask for the focused element's geometry when they + /// need it (e.g. to frame system text-interaction UI overlaid on the focused + /// element). + fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { + None + } + + /// Get the length of the document in UTF-16 characters, if known. + fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option { + None + } + + /// Allows a given input context to opt into getting raw key repeats instead of + /// sending these to the platform. + /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults + /// (which is how iTerm does it) but it doesn't seem to work for me. + #[allow(dead_code)] + fn apple_press_and_hold_enabled(&mut self) -> bool { + true + } + + /// Returns whether this handler is accepting text input to be inserted. + fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool { + true + } + + /// The contiguous range of text, in UTF-16 code units, that platform text + /// input may read and edit around the current selection. + /// + /// Platforms that mirror document text into an IME-editable buffer clamp + /// the mirrored window to this range, so multi-step IME edit gestures + /// (word deletion, autocorrect rewrites, suggestion picks) cannot reach + /// content outside it. The range should contain the current selection; + /// when it cannot (a selection spanning a region boundary), platforms + /// degrade the mirrored IME context rather than widening the range. + /// `None` places no bound. + fn text_input_editable_range( + &mut self, + _window: &mut Window, + _cx: &mut App, + ) -> Option> { + None + } + + /// Returns whether printable keys should be routed to the IME before keybinding + /// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME) + /// is active. This prevents multi-stroke keybindings like `jj` from intercepting + /// keys that the IME should compose. + /// + /// Defaults to `false`. The editor overrides this based on whether it expects + /// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`). + /// The terminal keeps the default `false` so that raw keys reach the terminal process. + fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool { + false + } + + /// Get this handler's preferences for platform text assistance. + /// + /// GPUI re-queries this every frame and forwards it to the platform window + /// only when it changes, so implementations must be cheap and may vary the + /// result with application state (e.g. with the cursor's position). + fn text_input_configuration( + &mut self, + _window: &mut Window, + _cx: &mut App, + ) -> TextInputConfiguration { + TextInputConfiguration::default() + } +} + +/// Platform text-assistance preferences for the focused text region. +/// +/// Returned by [`InputHandler::text_input_configuration`] and forwarded to the +/// platform whenever it changes; the platform maps the fields onto its native +/// input-session attributes (on web, DOM attributes of the hidden editable +/// element such as `autocorrect` and `enterkeyhint`). +/// +/// The default disables all text assistance and requests no particular action +/// key presentation. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TextInputConfiguration { + /// Whether the platform may automatically correct entered text. + pub autocorrect: bool, + /// How software keyboards automatically capitalize entered text. + pub autocapitalize: Autocapitalize, + /// Whether software keyboards may offer word suggestions and spellcheck. + pub suggestions: bool, + /// The action advertised on a software keyboard's confirm ("enter") key. + pub input_action: TextInputAction, +} + +/// Automatic capitalization applied by software keyboards. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Autocapitalize { + /// No automatic capitalization. + #[default] + None, + /// Capitalize the first letter of each word. + Words, + /// Capitalize the first letter of each sentence. + Sentences, + /// Capitalize every letter. + Characters, +} + +/// The action a software keyboard advertises on its confirm ("enter") key. +/// +/// This affects only how the key is presented (icon or label); pressing it is +/// still delivered as ordinary input. +/// +/// The variants are the HTML `enterkeyhint` attribute's value set +/// (), +/// which also maps onto Android's `IME_ACTION_*` constants and iOS's +/// `UIReturnKeyType`; [`TextInputAction::Unspecified`] means "emit no hint". +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TextInputAction { + /// Let the platform choose its default presentation. + #[default] + Unspecified, + /// Inserting a line break. + Enter, + /// Committing the field's value. + Done, + /// Navigating to the typed target. + Go, + /// Moving to the next field. + Next, + /// Moving to the previous field. + Previous, + /// Executing a search. + Search, + /// Sending a message. + Send, +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +pub struct WindowOptions { + /// Specifies the state and bounds of the window in screen coordinates. + /// - `None`: Inherit the bounds. + /// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size. + pub window_bounds: Option, + + /// The titlebar configuration of the window + pub titlebar: Option, + + /// Whether the window should be focused when created + pub focus: bool, + + /// Whether the window should be shown when created + pub show: bool, + + /// The kind of window to create + pub kind: WindowKind, + + /// Whether the window can be moved by the user. When `false`, the user cannot drag + /// the window (on macOS this sets `NSWindow.isMovable`, which also disables the + /// Window-menu tiling items); programmatic moves are still allowed. + pub is_movable: bool, + + /// Whether the application owns dragging of the (custom) titlebar, rather than + /// AppKit. Only has an effect on macOS. + /// + /// Set this to `true` for windows that draw their own titlebar and move the window + /// themselves via [`Window::start_window_move`]. It marks the whole content view as + /// app-owned titlebar content, so AppKit neither drags the window from the titlebar + /// nor delays titlebar clicks while disambiguating double-clicks (a delay first + /// observed on macOS 27). It is independent of `is_movable`, so such windows stay + /// user-movable (via their own drag) and keep the Window-menu tiling items enabled. + /// + /// Leave this `false` for windows that rely on AppKit's native titlebar dragging. + pub app_owns_titlebar_drag: bool, + + /// The minimum interval between animation frames while the window is inactive. + /// + /// Set to `None` to disable inactive-window animation frame throttling. + pub inactive_frame_interval: Option, + + /// Whether the window should be resizable by the user + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + pub is_minimizable: bool, + + /// The display to create the window on, if this is None, + /// the window will be created on the main display + pub display_id: Option, + + /// The appearance of the window background. + pub window_background: WindowBackgroundAppearance, + + /// Application identifier of the window. Can by used by desktop environments to group applications together. + pub app_id: Option, + + /// Window minimum size + pub window_min_size: Option>, + + /// Whether to use client or server-side decorations on X11 and Wayland. + /// The platform may ignore requests it cannot satisfy. + pub window_decorations: Option, + + /// Icon image (X11 only) + pub icon: Option>, + + /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. + pub tabbing_identifier: Option, +} + +/// The variables that can be configured when creating a new window +#[derive(Debug)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +#[allow(missing_docs)] +pub struct WindowParams { + pub bounds: Bounds, + + /// The titlebar configuration of the window + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub titlebar: Option, + + /// The kind of window to create + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub kind: WindowKind, + + /// Whether the window should be movable by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_movable: bool, + + /// Whether the application owns dragging of the (custom) titlebar (macOS only) + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + pub app_owns_titlebar_drag: bool, + + /// Whether the window should be resizable by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_resizable: bool, + + /// Whether the window should be minimized by the user + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub is_minimizable: bool, + + #[cfg_attr( + any(target_os = "linux", target_os = "freebsd", target_os = "windows"), + allow(dead_code) + )] + pub focus: bool, + + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + pub show: bool, + + /// An image to set as the window icon (x11 only) + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub icon: Option>, + + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub display_id: Option, + + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub app_id: Option, + + pub window_min_size: Option>, + + #[cfg(target_os = "macos")] + pub tabbing_identifier: Option, +} + +/// Represents the status of how a window should be opened. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum WindowBounds { + /// Indicates that the window should open in a windowed state with the given bounds. + Windowed(Bounds), + /// Indicates that the window should open in a maximized state. + /// The bounds provided here represent the restore size of the window. + Maximized(Bounds), + /// Indicates that the window should open in fullscreen mode. + /// The bounds provided here represent the restore size of the window. + Fullscreen(Bounds), +} + +impl Default for WindowBounds { + fn default() -> Self { + WindowBounds::Windowed(Bounds::default()) + } +} + +impl WindowBounds { + /// Retrieve the inner bounds + pub fn get_bounds(&self) -> Bounds { + match self { + WindowBounds::Windowed(bounds) => *bounds, + WindowBounds::Maximized(bounds) => *bounds, + WindowBounds::Fullscreen(bounds) => *bounds, + } + } + + /// Creates a new window bounds that centers the window on the screen. + pub fn centered(size: Size, cx: &App) -> Self { + WindowBounds::Windowed(Bounds::centered(None, size, cx)) + } +} + +impl Default for WindowOptions { + fn default() -> Self { + Self { + window_bounds: None, + titlebar: Some(TitlebarOptions { + title: Default::default(), + appears_transparent: Default::default(), + traffic_light_position: Default::default(), + }), + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + app_owns_titlebar_drag: false, + inactive_frame_interval: Some(Duration::from_micros(33_333)), + is_resizable: true, + is_minimizable: true, + display_id: None, + window_background: WindowBackgroundAppearance::default(), + icon: None, + app_id: None, + window_min_size: None, + window_decorations: None, + tabbing_identifier: None, + } + } +} + +/// The options that can be configured for a window's titlebar +#[derive(Debug, Default)] +pub struct TitlebarOptions { + /// The initial title of the window + pub title: Option, + + /// Should the default system titlebar be hidden to allow for a custom-drawn titlebar? (macOS and Windows only) + /// Refer to [`WindowOptions::window_decorations`] on Linux + pub appears_transparent: bool, + + /// The position of the macOS traffic light buttons + pub traffic_light_position: Option>, +} + +/// The kind of window to create +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WindowKind { + /// A normal application window + Normal, + + /// A window that appears above all other windows, usually used for alerts or popups + /// use sparingly! + PopUp, + + /// A parent-anchored, platform-native popup window for menus, comboboxes, context menus and + /// tooltips. Unlike [`WindowKind::PopUp`], it is positioned relative to a parent window. + /// + /// The popup's size comes from [`WindowOptions::window_bounds`], whose origin is ignored. + /// See [`popup::PopupOptions`] for the placement options. Platforms without a native + /// implementation reject it with [`popup::PopupNotSupportedError`]. + AnchoredPopup(popup::PopupOptions), + + /// A floating window that appears on top of its parent window + Floating, + + /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as + /// docks, notifications or wallpapers. + #[cfg(all(target_os = "linux", feature = "wayland"))] + LayerShell(layer_shell::LayerShellOptions), + + /// A window that appears on top of its parent window and blocks interaction with it + /// until the modal window is closed + Dialog, +} + +/// The appearance of the window, as defined by the operating system. +/// +/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) +/// values. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum WindowAppearance { + /// A light appearance. + /// + /// On macOS, this corresponds to the `aqua` appearance. + #[default] + Light, + + /// A light appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance. + VibrantLight, + + /// A dark appearance. + /// + /// On macOS, this corresponds to the `darkAqua` appearance. + Dark, + + /// A dark appearance with vibrant colors. + /// + /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance. + VibrantDark, +} + +/// The appearance of the background of the window itself, when there is +/// no content or the content is transparent. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum WindowBackgroundAppearance { + /// Opaque. + /// + /// This lets the window manager know that content behind this + /// window does not need to be drawn. + /// + /// Actual color depends on the system and themes should define a fully + /// opaque background color instead. + #[default] + Opaque, + /// Plain alpha transparency. + Transparent, + /// Transparency, but the contents behind the window are blurred. + /// + /// Not always supported. + Blurred, + /// The Mica backdrop material, supported on Windows 11. + MicaBackdrop, + /// The Mica Alt backdrop material, supported on Windows 11. + MicaAltBackdrop, +} + +/// The text rendering mode to use for drawing glyphs. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum TextRenderingMode { + /// Use the platform's default text rendering mode. + #[default] + PlatformDefault, + /// Use subpixel (ClearType-style) text rendering. + Subpixel, + /// Use grayscale text rendering. + Grayscale, +} + +/// The options that can be configured for a file dialog prompt +#[derive(Clone, Debug)] +pub struct PathPromptOptions { + /// Should the prompt allow files to be selected? + pub files: bool, + /// Should the prompt allow directories to be selected? + pub directories: bool, + /// Should the prompt allow multiple files to be selected? + pub multiple: bool, + /// The prompt to show to a user when selecting a path + pub prompt: Option, +} + +/// What kind of prompt styling to show +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PromptLevel { + /// A prompt that is shown when the user should be notified of something + Info, + + /// A prompt that is shown when the user needs to be warned of a potential problem + Warning, + + /// A prompt that is shown when a critical problem has occurred + Critical, +} + +/// Prompt Button +#[derive(Clone, Debug, PartialEq)] +pub enum PromptButton { + /// Ok button + Ok(SharedString), + /// Cancel button + Cancel(SharedString), + /// Other button + Other(SharedString), +} + +impl PromptButton { + /// Create a button with label + pub fn new(label: impl Into) -> Self { + PromptButton::Other(label.into()) + } + + /// Create an Ok button + pub fn ok(label: impl Into) -> Self { + PromptButton::Ok(label.into()) + } + + /// Create a Cancel button + pub fn cancel(label: impl Into) -> Self { + PromptButton::Cancel(label.into()) + } + + /// Returns true if this button is a cancel button. + #[allow(dead_code)] + pub fn is_cancel(&self) -> bool { + matches!(self, PromptButton::Cancel(_)) + } + + /// Returns the label of the button + pub fn label(&self) -> &SharedString { + match self { + PromptButton::Ok(label) => label, + PromptButton::Cancel(label) => label, + PromptButton::Other(label) => label, + } + } +} + +impl From<&str> for PromptButton { + fn from(value: &str) -> Self { + match value.to_lowercase().as_str() { + "ok" => PromptButton::Ok("OK".into()), + "cancel" => PromptButton::Cancel("Cancel".into()), + _ => PromptButton::Other(SharedString::from(value.to_owned())), + } + } +} + +/// The style of the cursor (pointer) +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub enum CursorStyle { + /// The default cursor + #[default] + Arrow, + + /// A text input cursor + /// corresponds to the CSS cursor value `text` + IBeam, + + /// A crosshair cursor + /// corresponds to the CSS cursor value `crosshair` + Crosshair, + + /// A closed hand cursor + /// corresponds to the CSS cursor value `grabbing` + ClosedHand, + + /// An open hand cursor + /// corresponds to the CSS cursor value `grab` + OpenHand, + + /// A pointing hand cursor + /// corresponds to the CSS cursor value `pointer` + PointingHand, + + /// A resize left cursor + /// corresponds to the CSS cursor value `w-resize` + ResizeLeft, + + /// A resize right cursor + /// corresponds to the CSS cursor value `e-resize` + ResizeRight, + + /// A resize cursor to the left and right + /// corresponds to the CSS cursor value `ew-resize` + ResizeLeftRight, + + /// A resize up cursor + /// corresponds to the CSS cursor value `n-resize` + ResizeUp, + + /// A resize down cursor + /// corresponds to the CSS cursor value `s-resize` + ResizeDown, + + /// A resize cursor directing up and down + /// corresponds to the CSS cursor value `ns-resize` + ResizeUpDown, + + /// A resize cursor directing up-left and down-right + /// corresponds to the CSS cursor value `nesw-resize` + ResizeUpLeftDownRight, + + /// A resize cursor directing up-right and down-left + /// corresponds to the CSS cursor value `nwse-resize` + ResizeUpRightDownLeft, + + /// A cursor indicating that the item/column can be resized horizontally. + /// corresponds to the CSS cursor value `col-resize` + ResizeColumn, + + /// A cursor indicating that the item/row can be resized vertically. + /// corresponds to the CSS cursor value `row-resize` + ResizeRow, + + /// A text input cursor for vertical layout + /// corresponds to the CSS cursor value `vertical-text` + IBeamCursorForVerticalLayout, + + /// A cursor indicating that the operation is not allowed + /// corresponds to the CSS cursor value `not-allowed` + OperationNotAllowed, + + /// A cursor indicating that the operation will result in a link + /// corresponds to the CSS cursor value `alias` + DragLink, + + /// A cursor indicating that the operation will result in a copy + /// corresponds to the CSS cursor value `copy` + DragCopy, + + /// A cursor indicating that the operation will result in a context menu + /// corresponds to the CSS cursor value `context-menu` + ContextualMenu, +} + +/// A clipboard item that should be copied to the clipboard +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardItem { + /// The entries in this clipboard item. + pub entries: Vec, +} + +/// An error produced by [`Platform::read_from_clipboard_async`]. +/// +/// Callers surface these failures to users, so the variants distinguish +/// conditions that call for different user-facing guidance. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ClipboardReadError { + /// The platform clipboard is not available in this context, e.g. the + /// browser does not expose the async clipboard API or the page is not a + /// secure context. + Unavailable, + /// The platform refused access, e.g. the user declined the browser's + /// clipboard permission prompt or paste confirmation. + Denied(String), + /// The clipboard contents could not be converted into a + /// [`ClipboardItem`]. + UnsupportedContent, +} + +impl std::fmt::Display for ClipboardReadError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unavailable => formatter.write_str("the clipboard is unavailable"), + Self::Denied(message) => { + write!(formatter, "clipboard access was denied: {message}") + } + Self::UnsupportedContent => { + formatter.write_str("the clipboard contents are unsupported") + } + } + } +} + +impl std::error::Error for ClipboardReadError {} + +/// Either a ClipboardString or a ClipboardImage +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ClipboardEntry { + /// A string entry + String(ClipboardString), + /// An image entry + Image(Image), + /// A file entry + ExternalPaths(crate::ExternalPaths), +} + +impl ClipboardItem { + /// Create a new ClipboardItem::String with no associated metadata + pub fn new_string(text: String) -> Self { + Self { + entries: vec![ClipboardEntry::String(ClipboardString::new(text))], + } + } + + /// Create a new ClipboardItem::String with the given text and associated metadata + pub fn new_string_with_metadata(text: String, metadata: String) -> Self { + Self { + entries: vec![ClipboardEntry::String(ClipboardString { + text, + metadata: Some(metadata), + })], + } + } + + /// Create a new ClipboardItem::String with the given text and associated metadata + pub fn new_string_with_json_metadata(text: String, metadata: T) -> Self { + Self { + entries: vec![ClipboardEntry::String( + ClipboardString::new(text).with_json_metadata(metadata), + )], + } + } + + /// Create a new ClipboardItem::Image with the given image with no associated metadata + pub fn new_image(image: &Image) -> Self { + Self { + entries: vec![ClipboardEntry::Image(image.clone())], + } + } + + /// Concatenates together all the ClipboardString entries in the item. + /// Returns None if there were no ClipboardString entries. + pub fn text(&self) -> Option { + let mut answer = String::new(); + + for entry in self.entries.iter() { + if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry { + answer.push_str(text); + } + } + + if answer.is_empty() { + for entry in self.entries.iter() { + if let ClipboardEntry::ExternalPaths(paths) = entry { + for path in &paths.0 { + use std::fmt::Write as _; + _ = write!(answer, "{}", path.display()); + } + } + } + } + + if !answer.is_empty() { + Some(answer) + } else { + None + } + } + + /// If this item is one ClipboardEntry::String, returns its metadata. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + pub fn metadata(&self) -> Option<&String> { + match self.entries().first() { + Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => { + clipboard_string.metadata.as_ref() + } + _ => None, + } + } + + /// Get the item's entries + pub fn entries(&self) -> &[ClipboardEntry] { + &self.entries + } + + /// Get owned versions of the item's entries + pub fn into_entries(self) -> impl Iterator { + self.entries.into_iter() + } +} + +impl From for ClipboardEntry { + fn from(value: ClipboardString) -> Self { + Self::String(value) + } +} + +impl From for ClipboardEntry { + fn from(value: String) -> Self { + Self::from(ClipboardString::from(value)) + } +} + +impl From for ClipboardEntry { + fn from(value: Image) -> Self { + Self::Image(value) + } +} + +impl From for ClipboardItem { + fn from(value: ClipboardEntry) -> Self { + Self { + entries: vec![value], + } + } +} + +impl From for ClipboardItem { + fn from(value: String) -> Self { + Self::from(ClipboardEntry::from(value)) + } +} + +impl From for ClipboardItem { + fn from(value: Image) -> Self { + Self::from(ClipboardEntry::from(value)) + } +} + +/// One of the editor's supported image formats (e.g. PNG, JPEG) - used when dealing with images in the clipboard +#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)] +pub enum ImageFormat { + // Sorted from most to least likely to be pasted into an editor, + // which matters when we iterate through them trying to see if + // clipboard content matches them. + /// .png + Png, + /// .jpeg or .jpg + Jpeg, + /// .webp + Webp, + /// .gif + Gif, + /// .svg + Svg, + /// .bmp + Bmp, + /// .tif or .tiff + Tiff, + /// .ico + Ico, + /// Netpbm image formats (.pbm, .ppm, .pgm). + Pnm, +} + +impl ImageFormat { + /// Returns the mime type for the ImageFormat + pub const fn mime_type(self) -> &'static str { + match self { + ImageFormat::Png => "image/png", + ImageFormat::Jpeg => "image/jpeg", + ImageFormat::Webp => "image/webp", + ImageFormat::Gif => "image/gif", + ImageFormat::Svg => "image/svg+xml", + ImageFormat::Bmp => "image/bmp", + ImageFormat::Tiff => "image/tiff", + ImageFormat::Ico => "image/ico", + ImageFormat::Pnm => "image/x-portable-anymap", + } + } + + /// Returns the file extension for this image format (without leading dot). + pub const fn extension(self) -> &'static str { + match self { + ImageFormat::Png => "png", + ImageFormat::Jpeg => "jpg", + ImageFormat::Webp => "webp", + ImageFormat::Gif => "gif", + ImageFormat::Svg => "svg", + ImageFormat::Bmp => "bmp", + ImageFormat::Tiff => "tiff", + ImageFormat::Ico => "ico", + ImageFormat::Pnm => "pnm", + } + } + + /// Returns the ImageFormat for the given mime type, including known aliases. + pub fn from_mime_type(mime_type: &str) -> Option { + use strum::IntoEnumIterator; + Self::iter() + .find(|format| format.mime_type() == mime_type) + .or_else(|| Self::from_mime_type_alias(mime_type)) + } + + /// Non-canonical mime types that some producers use in the wild. + /// Unlike `mime_type()` which returns the single canonical form, + /// these are legacy or shortened variants we still need to recognize. + fn from_mime_type_alias(mime_type: &str) -> Option { + match mime_type { + "image/jpg" => Some(Self::Jpeg), + "image/tif" => Some(Self::Tiff), + _ => None, + } + } +} + +/// An image, with a format and certain bytes +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Image { + /// The image format the bytes represent (e.g. PNG) + pub format: ImageFormat, + /// The raw image bytes + pub bytes: Vec, + /// The unique ID for the image + pub id: u64, +} + +pub(crate) fn decode_static_image( + bytes: &[u8], + format: image::ImageFormat, +) -> Result> { + let decoder = image::ImageReader::with_format(Cursor::new(bytes), format) + .into_decoder() + .context("creating image decoder")?; + decode_static_image_from_decoder(decoder) +} + +pub(crate) fn decode_static_image_from_decoder( + mut decoder: impl image::ImageDecoder, +) -> Result> { + let orientation = decoder + .orientation() + .context("reading decoder's orientation")?; + let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?; + image.apply_orientation(orientation); + + let mut data = image.into_rgba8(); + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + Ok(SmallVec::from_elem(Frame::new(data), 1)) +} + +impl Hash for Image { + fn hash(&self, state: &mut H) { + state.write_u64(self.id); + } +} + +impl Image { + /// An empty image containing no data + pub fn empty() -> Self { + Self::from_bytes(ImageFormat::Png, Vec::new()) + } + + /// Create an image from a format and bytes + pub fn from_bytes(format: ImageFormat, bytes: Vec) -> Self { + Self { + id: hash(&bytes), + format, + bytes, + } + } + + /// Get this image's ID + pub fn id(&self) -> u64 { + self.id + } + + /// Use the GPUI `use_asset` API to make this image renderable + pub fn use_render_image( + self: Arc, + window: &mut Window, + cx: &mut App, + ) -> Option> { + ImageSource::Image(self) + .use_data(None, window, cx) + .and_then(|result| result.ok()) + } + + /// Use the GPUI `get_asset` API to make this image renderable + pub fn get_render_image( + self: Arc, + window: &mut Window, + cx: &mut App, + ) -> Option> { + ImageSource::Image(self) + .get_data(None, window, cx) + .and_then(|result| result.ok()) + } + + /// Use the GPUI `remove_asset` API to drop this image, if possible. + pub fn remove_asset(self: Arc, cx: &mut App) { + ImageSource::Image(self).remove_asset(cx); + } + + /// Check whether this image is present in GPUI's asset cache (loading or + /// loaded), without fetching it. + #[cfg(any(test, feature = "test-support"))] + pub fn is_asset_cached(self: &Arc, cx: &App) -> bool { + ImageSource::Image(self.clone()).is_asset_cached(cx) + } + + /// Convert the clipboard image to an `ImageData` object. + pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result> { + let frames = match self.format { + ImageFormat::Gif => { + let decoder = GifDecoder::new(Cursor::new(&self.bytes))?; + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + match frame { + Ok(mut frame) => { + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + Err(err) => { + log::debug!("Skipping GIF frame due to decode error: {err}"); + } + } + } + + if frames.is_empty() { + anyhow::bail!("GIF could not be decoded: all frames failed"); + } + + frames + } + ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?, + ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?, + ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?, + ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?, + ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?, + ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?, + ImageFormat::Svg => { + return svg_renderer + .render_single_frame(&self.bytes, 1.0) + .map_err(Into::into); + } + ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?, + }; + + Ok(Arc::new(RenderImage::new(frames))) + } + + /// Get the format of the clipboard image + pub fn format(&self) -> ImageFormat { + self.format + } + + /// Get the raw bytes of the clipboard image + pub fn bytes(&self) -> &[u8] { + self.bytes.as_slice() + } +} + +/// A clipboard item that should be copied to the clipboard +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClipboardString { + /// The text content. + pub text: String, + /// Optional metadata associated with this clipboard string. + pub metadata: Option, +} + +impl ClipboardString { + /// Create a new clipboard string with the given text + pub fn new(text: String) -> Self { + Self { + text, + metadata: None, + } + } + + /// Return a new clipboard item with the metadata replaced by the given metadata, + /// after serializing it as JSON. + pub fn with_json_metadata(mut self, metadata: T) -> Self { + self.metadata = Some(serde_json::to_string(&metadata).unwrap()); + self + } + + /// Get the text of the clipboard string + pub fn text(&self) -> &String { + &self.text + } + + /// Get the owned text of the clipboard string + pub fn into_text(self) -> String { + self.text + } + + /// Get the metadata of the clipboard string, formatted as JSON + pub fn metadata_json(&self) -> Option + where + T: for<'a> Deserialize<'a>, + { + self.metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).ok()) + } + + #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] + /// Compute a hash of the given text for clipboard change detection. + pub fn text_hash(text: &str) -> u64 { + let mut hasher = SeaHasher::new(); + text.hash(&mut hasher); + hasher.finish() + } +} + +impl From for ClipboardString { + fn from(value: String) -> Self { + Self { + text: value, + metadata: None, + } + } +} + +#[cfg(test)] +mod image_tests { + use super::*; + use std::sync::Arc; + + #[test] + fn test_image_to_image_data_applies_exif_orientation() { + let image = Image::from_bytes( + ImageFormat::Jpeg, + include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(), + ); + + let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); + + assert_eq!(render_image.size(0), size(16.into(), 32.into())); + + let bytes = render_image.as_bytes(0).unwrap(); + assert_eq!(&bytes[..4], &[255, 255, 255, 255]); + assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]); + } + + #[test] + fn test_svg_image_to_image_data_converts_to_bgra() { + let image = Image::from_bytes( + ImageFormat::Svg, + br##" + +"## + .to_vec(), + ); + + let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap(); + let bytes = render_image.as_bytes(0).unwrap(); + + for pixel in bytes.chunks_exact(4) { + assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]); + } + } +} + +#[cfg(test)] +mod atlas_tests { + use super::*; + + const TILE_SIZE: Size = Size { + width: DevicePixels(1), + height: DevicePixels(1), + }; + + #[derive(Default)] + struct RecordingAtlasBackend { + insert_calls: u32, + fail_next_insert: bool, + removed_tiles: Vec, + } + + impl AtlasBackend for RecordingAtlasBackend { + fn insert( + &mut self, + kind: AtlasTextureKind, + size: Size, + _bytes: &[u8], + ) -> Result { + self.insert_calls += 1; + if std::mem::take(&mut self.fail_next_insert) { + anyhow::bail!("backend failed"); + } + Ok(AtlasTile { + texture_id: AtlasTextureId { index: 0, kind }, + tile_id: TileId(self.insert_calls), + padding: 0, + bounds: Bounds { + origin: Point::default(), + size, + }, + }) + } + + fn remove(&mut self, tile: AtlasTile) { + self.removed_tiles.push(tile); + } + } + + fn image_key(image_id: usize) -> AtlasKey { + AtlasKey::Image(RenderImageParams { + image_id: crate::ImageId(image_id), + frame_index: 0, + }) + } + + fn build_tile() -> Result, Cow<'static, [u8]>)>> { + Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255])))) + } + + #[test] + fn only_successful_inserts_are_cached() -> Result<()> { + let mut state = AtlasState::new(RecordingAtlasBackend::default()); + let key = image_key(1); + + assert_eq!( + state.get_or_insert_with(key.clone(), &mut || Ok(None))?, + None + ); + state + .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed")) + .expect_err("builder error should propagate"); + assert!(!state.contains(&key)); + assert_eq!(state.backend.insert_calls, 0); + + state.backend.fail_next_insert = true; + state + .get_or_insert_with(key.clone(), &mut build_tile) + .expect_err("backend error should propagate"); + assert!(!state.contains(&key)); + assert_eq!(state.backend.insert_calls, 1); + + let tile = state + .get_or_insert_with(key.clone(), &mut build_tile)? + .context("builder should produce a tile")?; + assert_eq!(tile.texture_id.kind, key.texture_kind()); + assert_eq!( + state.get_or_insert_with(key.clone(), &mut || { + anyhow::bail!("cache hit must not call the builder") + })?, + Some(tile) + ); + assert!(state.contains(&key)); + assert_eq!(state.backend.insert_calls, 2); + Ok(()) + } + + #[test] + fn remove_and_clear_invalidate_keys() -> Result<()> { + let mut state = AtlasState::new(RecordingAtlasBackend::default()); + let key = image_key(1); + let other_key = image_key(2); + let tile = state + .get_or_insert_with(key.clone(), &mut build_tile)? + .context("builder should produce a tile")?; + state + .get_or_insert_with(other_key.clone(), &mut build_tile)? + .context("builder should produce another tile")?; + + state.remove(&key); + state.remove(&key); + assert!(!state.contains(&key)); + assert!(state.contains(&other_key)); + assert_eq!(state.backend.removed_tiles, vec![tile]); + + let mut reset_calls = 0; + state.clear(|_| reset_calls += 1); + assert_eq!(reset_calls, 1); + assert!(!state.contains(&other_key)); + assert_eq!(state.backend.removed_tiles, vec![tile]); + Ok(()) + } +} + +#[cfg(test)] +mod frame_signal_tests { + use super::*; + + #[cfg(feature = "profiler")] + #[test] + fn coalesced_signals_retain_the_earliest_until_drained() { + for (first_source, later_source) in [ + ( + FrameRequestSource::NativeCallback, + FrameRequestSource::LocalSchedule, + ), + ( + FrameRequestSource::LocalSchedule, + FrameRequestSource::NativeCallback, + ), + ] { + let signal = PlatformFrameSignal::new(); + let first = + PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests"); + assert_eq!(signal.take(), None); + signal.record(first + Duration::from_millis(16), later_source); + signal.record(first, first_source); + signal.record(first + Duration::from_millis(32), later_source); + assert_eq!(signal.take(), Some((first, first_source))); + assert_eq!(signal.take(), None); + let next = first + Duration::from_millis(48); + signal.record(next, later_source); + assert_eq!(signal.take(), Some((next, later_source))); + } + } + + #[cfg(not(feature = "profiler"))] + #[test] + fn disabled_profiling_does_not_capture_or_store_timestamps() { + let captured = PlatformFrameSignal::capture(|| { + panic!("profiler-disabled builds must not evaluate the capture closure"); + }); + assert_eq!(captured, None); + let signal = PlatformFrameSignal::new(); + signal.record(Instant::now(), FrameRequestSource::LocalSchedule); + assert_eq!(signal.take(), None); + } +} + +#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))] +mod tests { + use super::*; + use std::collections::HashSet; + + #[test] + fn test_window_button_layout_parse_standard() { + let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap(); + assert_eq!( + layout.left, + [ + Some(WindowButton::Close), + Some(WindowButton::Minimize), + None + ] + ); + assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); + } + + #[test] + fn test_window_button_layout_parse_right_only() { + let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap(); + assert_eq!(layout.left, [None, None, None]); + assert_eq!( + layout.right, + [ + Some(WindowButton::Minimize), + Some(WindowButton::Maximize), + Some(WindowButton::Close) + ] + ); + } + + #[test] + fn test_window_button_layout_parse_left_only() { + let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap(); + assert_eq!( + layout.left, + [ + Some(WindowButton::Close), + Some(WindowButton::Minimize), + Some(WindowButton::Maximize) + ] + ); + assert_eq!(layout.right, [None, None, None]); + } + + #[test] + fn test_window_button_layout_parse_with_whitespace() { + let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap(); + assert_eq!( + layout.left, + [ + Some(WindowButton::Close), + Some(WindowButton::Minimize), + None + ] + ); + assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); + } + + #[test] + fn test_window_button_layout_parse_empty() { + let layout = WindowButtonLayout::parse("").unwrap(); + assert_eq!(layout.left, [None, None, None]); + assert_eq!(layout.right, [None, None, None]); + } + + #[test] + fn test_window_button_layout_parse_intentionally_empty() { + let layout = WindowButtonLayout::parse(":").unwrap(); + assert_eq!(layout.left, [None, None, None]); + assert_eq!(layout.right, [None, None, None]); + } + + #[test] + fn test_window_button_layout_parse_invalid_buttons() { + let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap(); + assert_eq!( + layout.left, + [ + Some(WindowButton::Close), + Some(WindowButton::Minimize), + None + ] + ); + assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); + } + + #[test] + fn test_window_button_layout_parse_deduplicates_same_side_buttons() { + let layout = WindowButtonLayout::parse("close,close,minimize").unwrap(); + assert_eq!( + layout.right, + [ + Some(WindowButton::Close), + Some(WindowButton::Minimize), + None + ] + ); + assert_eq!(layout.format(), ":close,minimize"); + } + + #[test] + fn test_window_button_layout_parse_deduplicates_buttons_across_sides() { + let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap(); + assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); + assert_eq!( + layout.right, + [ + Some(WindowButton::Maximize), + Some(WindowButton::Minimize), + None + ] + ); + + let button_ids: Vec<_> = layout + .left + .iter() + .chain(layout.right.iter()) + .flatten() + .map(WindowButton::id) + .collect(); + let unique_button_ids = button_ids.iter().copied().collect::>(); + assert_eq!(unique_button_ids.len(), button_ids.len()); + assert_eq!(layout.format(), "close:maximize,minimize"); + } + + #[test] + fn test_window_button_layout_parse_gnome_style() { + let layout = WindowButtonLayout::parse("close").unwrap(); + assert_eq!(layout.left, [None, None, None]); + assert_eq!(layout.right, [Some(WindowButton::Close), None, None]); + } + + #[test] + fn test_window_button_layout_parse_elementary_style() { + let layout = WindowButtonLayout::parse("close:maximize").unwrap(); + assert_eq!(layout.left, [Some(WindowButton::Close), None, None]); + assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]); + } + + #[test] + fn test_window_button_layout_round_trip() { + let cases = [ + "close:minimize,maximize", + "minimize,maximize,close:", + ":close", + "close:", + "close:maximize", + ":", + ]; + + for case in cases { + let layout = WindowButtonLayout::parse(case).unwrap(); + assert_eq!(layout.format(), case, "Round-trip failed for: {}", case); + } + } + + #[test] + fn test_window_button_layout_linux_default() { + let layout = WindowButtonLayout::linux_default(); + assert_eq!(layout.left, [None, None, None]); + assert_eq!( + layout.right, + [ + Some(WindowButton::Minimize), + Some(WindowButton::Maximize), + Some(WindowButton::Close) + ] + ); + + let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap(); + assert_eq!(round_tripped, layout); + } + + #[test] + fn test_window_button_layout_parse_all_invalid() { + assert!(WindowButtonLayout::parse("asdfghjkl").is_err()); + } +} diff --git a/vendor/gpui-pre/src/platform/app_menu.rs b/vendor/gpui-pre/src/platform/app_menu.rs new file mode 100644 index 0000000..27c20c0 --- /dev/null +++ b/vendor/gpui-pre/src/platform/app_menu.rs @@ -0,0 +1,426 @@ +use crate::{Action, App, Platform, SharedString}; + +/// A menu of the application, either a main menu or a submenu +pub struct Menu { + /// The name of the menu + pub name: SharedString, + + /// The items in the menu + pub items: Vec, + + /// Whether this menu is disabled + pub disabled: bool, +} + +impl Menu { + /// Create a new Menu with the given name + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + items: vec![], + disabled: false, + } + } + + /// Set items to be in this menu + pub fn items(mut self, items: impl IntoIterator) -> Self { + self.items = items.into_iter().collect(); + self + } + + /// Set whether this menu is disabled + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + /// Create an OwnedMenu from this Menu + pub fn owned(self) -> OwnedMenu { + OwnedMenu { + name: self.name.to_string().into(), + items: self.items.into_iter().map(|item| item.owned()).collect(), + disabled: self.disabled, + } + } +} + +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +pub struct OsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + +impl OsMenu { + /// Create an OwnedOsMenu from this OsMenu + pub fn owned(self) -> OwnedOsMenu { + OwnedOsMenu { + name: self.name.to_string().into(), + menu_type: self.menu_type, + } + } +} + +/// The type of system menu +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum SystemMenuType { + /// The 'Services' menu in the Application menu on macOS + Services, +} + +/// The different kinds of items that can be in a menu +pub enum MenuItem { + /// A separator between items + Separator, + + /// A submenu + Submenu(Menu), + + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OsMenu), + + /// An action that can be performed + Action { + /// The name of this menu item + name: SharedString, + + /// The action to perform when this menu item is selected + action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information + os_action: Option, + + /// Whether this action is checked + checked: bool, + + /// Whether this action is disabled + disabled: bool, + }, +} + +impl MenuItem { + /// Creates a new menu item that is a separator + pub fn separator() -> Self { + Self::Separator + } + + /// Creates a new menu item that is a submenu + pub fn submenu(menu: Menu) -> Self { + Self::Submenu(menu) + } + + /// Creates a new submenu that is populated by the OS + pub fn os_submenu(name: impl Into, menu_type: SystemMenuType) -> Self { + Self::SystemMenu(OsMenu { + name: name.into(), + menu_type, + }) + } + + /// Creates a new menu item that invokes an action + pub fn action(name: impl Into, action: impl Action) -> Self { + Self::Action { + name: name.into(), + action: Box::new(action), + os_action: None, + checked: false, + disabled: false, + } + } + + /// Creates a new menu item that invokes an action and has an OS action + pub fn os_action( + name: impl Into, + action: impl Action, + os_action: OsAction, + ) -> Self { + Self::Action { + name: name.into(), + action: Box::new(action), + os_action: Some(os_action), + checked: false, + disabled: false, + } + } + + /// Create an OwnedMenuItem from this MenuItem + pub fn owned(self) -> OwnedMenuItem { + match self { + MenuItem::Separator => OwnedMenuItem::Separator, + MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()), + MenuItem::Action { + name, + action, + os_action, + checked, + disabled, + } => OwnedMenuItem::Action { + name: name.into(), + action, + os_action, + checked, + disabled, + }, + MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), + } + } + + /// Set whether this menu item is checked + /// + /// Only for [`MenuItem::Action`], otherwise, will be ignored + pub fn checked(mut self, checked: bool) -> Self { + match &mut self { + MenuItem::Action { checked: old, .. } => { + *old = checked; + } + _ => {} + } + self + } + + /// Returns whether this menu item is checked + /// + /// Only for [`MenuItem::Action`], otherwise, returns false + #[inline] + pub fn is_checked(&self) -> bool { + match self { + MenuItem::Action { checked, .. } => *checked, + _ => false, + } + } + + /// Set whether this menu item is disabled + pub fn disabled(mut self, disabled: bool) -> Self { + match &mut self { + MenuItem::Action { disabled: old, .. } => { + *old = disabled; + } + MenuItem::Submenu(submenu) => { + submenu.disabled = disabled; + } + _ => {} + } + self + } + + /// Returns whether this menu item is disabled + /// + /// Only for [`MenuItem::Action`] and [`MenuItem::Submenu`], otherwise, returns false + #[inline] + pub fn is_disabled(&self) -> bool { + match self { + MenuItem::Action { disabled, .. } => *disabled, + MenuItem::Submenu(submenu) => submenu.disabled, + _ => false, + } + } +} + +/// OS menus are menus that are recognized by the operating system +/// This allows the operating system to provide specialized items for +/// these menus +#[derive(Clone)] +pub struct OwnedOsMenu { + /// The name of the menu + pub name: SharedString, + + /// The type of menu + pub menu_type: SystemMenuType, +} + +/// A menu of the application, either a main menu or a submenu +#[derive(Clone)] +pub struct OwnedMenu { + /// The name of the menu + pub name: SharedString, + + /// The items in the menu + pub items: Vec, + + /// Whether this menu is disabled + pub disabled: bool, +} + +/// The different kinds of items that can be in a menu +pub enum OwnedMenuItem { + /// A separator between items + Separator, + + /// A submenu + Submenu(OwnedMenu), + + /// A menu, managed by the system (for example, the Services menu on macOS) + SystemMenu(OwnedOsMenu), + + /// An action that can be performed + Action { + /// The name of this menu item + name: String, + + /// The action to perform when this menu item is selected + action: Box, + + /// The OS Action that corresponds to this action, if any + /// See [`OsAction`] for more information + os_action: Option, + + /// Whether this action is checked + checked: bool, + + /// Whether this action is disabled + disabled: bool, + }, +} + +impl Clone for OwnedMenuItem { + fn clone(&self) -> Self { + match self { + OwnedMenuItem::Separator => OwnedMenuItem::Separator, + OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()), + OwnedMenuItem::Action { + name, + action, + os_action, + checked, + disabled, + } => OwnedMenuItem::Action { + name: name.clone(), + action: action.boxed_clone(), + os_action: *os_action, + checked: *checked, + disabled: *disabled, + }, + OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), + } + } +} + +// TODO: As part of the global selections refactor, these should +// be moved to GPUI-provided actions that make this association +// without leaking the platform details to GPUI users + +/// OS actions are actions that are recognized by the operating system +/// This allows the operating system to provide specialized behavior for +/// these actions +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + /// The 'cut' action + Cut, + + /// The 'copy' action + Copy, + + /// The 'paste' action + Paste, + + /// The 'select all' action + SelectAll, + + /// The 'undo' action + Undo, + + /// The 'redo' action + Redo, +} + +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) { + platform.on_will_open_app_menu(Box::new({ + let cx = cx.to_async(); + move || { + if let Some(app) = cx.app.upgrade() { + app.borrow_mut().update(|cx| cx.clear_pending_keystrokes()); + } + } + })); + + platform.on_validate_app_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.app + .upgrade() + .map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action))) + .unwrap_or(false) + } + })); + + platform.on_app_menu_action(Box::new({ + let cx = cx.to_async(); + move |action| { + if let Some(app) = cx.app.upgrade() { + app.borrow_mut().update(|cx| cx.dispatch_action(action)); + } + } + })); +} + +#[cfg(test)] +mod tests { + use crate::Menu; + + #[test] + fn test_menu() { + let menu = Menu::new("App") + .items(vec![ + crate::MenuItem::action("Action 1", gpui::NoAction), + crate::MenuItem::separator(), + ]) + .disabled(true); + + assert_eq!(menu.name.as_ref(), "App"); + assert_eq!(menu.items.len(), 2); + assert!(menu.disabled); + } + + #[test] + fn test_menu_item_builder() { + use super::MenuItem; + + let item = MenuItem::action("Test Action", gpui::NoAction); + assert_eq!( + match &item { + MenuItem::Action { name, .. } => name.as_ref(), + _ => unreachable!(), + }, + "Test Action" + ); + assert!(matches!( + item, + MenuItem::Action { + checked: false, + disabled: false, + .. + } + )); + + assert!( + MenuItem::action("Test Action", gpui::NoAction) + .checked(true) + .is_checked() + ); + assert!( + MenuItem::action("Test Action", gpui::NoAction) + .disabled(true) + .is_disabled() + ); + + let submenu = MenuItem::submenu(super::Menu { + name: "Submenu".into(), + items: vec![], + disabled: true, + }); + assert_eq!( + match &submenu { + MenuItem::Submenu(menu) => menu.name.as_ref(), + _ => unreachable!(), + }, + "Submenu" + ); + assert!(!submenu.is_checked()); + assert!(submenu.is_disabled()); + } +} diff --git a/vendor/gpui-pre/src/platform/keyboard.rs b/vendor/gpui-pre/src/platform/keyboard.rs new file mode 100644 index 0000000..10b8620 --- /dev/null +++ b/vendor/gpui-pre/src/platform/keyboard.rs @@ -0,0 +1,41 @@ +use collections::HashMap; + +use crate::{KeybindingKeystroke, Keystroke}; + +/// A trait for platform-specific keyboard layouts +pub trait PlatformKeyboardLayout { + /// Get the keyboard layout ID, which should be unique to the layout + fn id(&self) -> &str; + /// Get the keyboard layout display name + fn name(&self) -> &str; +} + +/// A trait for platform-specific keyboard mappings +pub trait PlatformKeyboardMapper { + /// Map a key equivalent to its platform-specific representation + fn map_key_equivalent( + &self, + keystroke: Keystroke, + use_key_equivalents: bool, + ) -> KeybindingKeystroke; + /// Get the key equivalents for the current keyboard layout, + /// only used on macOS + fn get_key_equivalents(&self) -> Option<&HashMap>; +} + +/// A dummy implementation of the platform keyboard mapper +pub struct DummyKeyboardMapper; + +impl PlatformKeyboardMapper for DummyKeyboardMapper { + fn map_key_equivalent( + &self, + keystroke: Keystroke, + _use_key_equivalents: bool, + ) -> KeybindingKeystroke { + KeybindingKeystroke::from_keystroke(keystroke) + } + + fn get_key_equivalents(&self) -> Option<&HashMap> { + None + } +} diff --git a/vendor/gpui-pre/src/platform/keystroke.rs b/vendor/gpui-pre/src/platform/keystroke.rs new file mode 100644 index 0000000..c24433d --- /dev/null +++ b/vendor/gpui-pre/src/platform/keystroke.rs @@ -0,0 +1,782 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + fmt::{Display, Write}, +}; + +use crate::PlatformKeyboardMapper; + +/// This is a helper trait so that we can simplify the implementation of some functions +pub trait AsKeystroke { + /// Returns the GPUI representation of the keystroke. + fn as_keystroke(&self) -> &Keystroke; +} + +/// A keystroke and associated metadata generated by the platform +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Keystroke { + /// the state of the modifier keys at the time the keystroke was generated + pub modifiers: Modifiers, + + /// key is the character printed on the key that was pressed + /// e.g. for option-s, key is "s" + /// On layouts that do not have ascii keys (e.g. Thai) + /// this will be the ASCII-equivalent character (q instead of ๆ), + /// and the typed character will be present in key_char. + pub key: String, + + /// key_char is the character that could have been typed when + /// this binding was pressed. + /// e.g. for s this is "s", for option-s "ß", and cmd-s None + pub key_char: Option, +} + +/// Represents a keystroke that can be used in keybindings and displayed to the user. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeybindingKeystroke { + /// The GPUI representation of the keystroke. + inner: Keystroke, + /// The modifiers to display. + #[cfg(target_os = "windows")] + display_modifiers: Modifiers, + /// The key to display. + #[cfg(target_os = "windows")] + display_key: String, +} + +/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use +/// markdown to display it. +#[derive(Debug)] +pub struct InvalidKeystrokeError { + /// The invalid keystroke. + pub keystroke: String, +} + +impl Error for InvalidKeystrokeError {} + +impl Display for InvalidKeystrokeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Invalid keystroke \"{}\". {}", + self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE + ) + } +} + +/// Sentence explaining what keystroke parser expects, starting with "Expected ..." +pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \ + (`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \ + followed by a key, separated by `-`."; + +impl Keystroke { + /// When matching a key we cannot know whether the user intended to type + /// the key_char or the key itself. On some non-US keyboards keys we use in our + /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + /// and on some keyboards the IME handler converts a sequence of keys into a + /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). + /// + /// This method assumes that `self` was typed and `target' is in the keymap, and checks + /// both possibilities for self against the target. + pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + #[cfg(not(target_os = "windows"))] + if let Some(key_char) = self + .key_char + .as_ref() + .filter(|key_char| key_char != &&self.key) + { + let ime_modifiers = Modifiers { + control: self.modifiers.control, + platform: self.modifiers.platform, + ..Default::default() + }; + + if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + return true; + } + } + + #[cfg(target_os = "windows")] + if let Some(key_char) = self + .key_char + .as_ref() + .filter(|key_char| key_char != &&self.key) + { + // On Windows, if key_char is set, then the typed keystroke produced the key_char + if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + return true; + } + } + + target.inner.modifiers == self.modifiers && target.inner.key == self.key + } + + /// key syntax is: + /// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char] + /// key_char syntax is only used for generating test events, + /// secondary means "cmd" on macOS and "ctrl" on other platforms + /// when matching a key with an key_char set will be matched without it. + pub fn parse(source: &str) -> std::result::Result { + let mut modifiers = Modifiers::none(); + let mut key = None; + let mut key_char = None; + + let mut components = source.split('-').peekable(); + while let Some(component) = components.next() { + if component.eq_ignore_ascii_case("ctrl") { + modifiers.control = true; + continue; + } + if component.eq_ignore_ascii_case("alt") { + modifiers.alt = true; + continue; + } + if component.eq_ignore_ascii_case("shift") { + modifiers.shift = true; + continue; + } + if component.eq_ignore_ascii_case("fn") { + modifiers.function = true; + continue; + } + if component.eq_ignore_ascii_case("secondary") { + if cfg!(target_os = "macos") { + modifiers.platform = true; + } else { + modifiers.control = true; + }; + continue; + } + + let is_platform = component.eq_ignore_ascii_case("cmd") + || component.eq_ignore_ascii_case("super") + || component.eq_ignore_ascii_case("win"); + + if is_platform { + modifiers.platform = true; + continue; + } + + let mut key_str = component.to_string(); + + if let Some(next) = components.peek() { + if next.is_empty() && source.ends_with('-') { + key = Some(String::from("-")); + break; + } else if next.len() > 1 && next.starts_with('>') { + key = Some(key_str); + key_char = Some(String::from(&next[1..])); + components.next(); + } else { + return Err(InvalidKeystrokeError { + keystroke: source.to_owned(), + }); + } + continue; + } + + if component.len() == 1 && component.as_bytes()[0].is_ascii_uppercase() { + // Convert to shift + lowercase char + modifiers.shift = true; + key_str.make_ascii_lowercase(); + } else { + // convert ascii chars to lowercase so that named keys like "tab" and "enter" + // are accepted case insensitively and stored how we expect so they are matched properly + key_str.make_ascii_lowercase() + } + key = Some(key_str); + } + + // Allow for the user to specify a keystroke modifier as the key itself + // This sets the `key` to the modifier, and disables the modifier + key = key.or_else(|| { + use std::mem; + // std::mem::take clears bool incase its true + if mem::take(&mut modifiers.shift) { + Some("shift".to_string()) + } else if mem::take(&mut modifiers.control) { + Some("control".to_string()) + } else if mem::take(&mut modifiers.alt) { + Some("alt".to_string()) + } else if mem::take(&mut modifiers.platform) { + Some("platform".to_string()) + } else if mem::take(&mut modifiers.function) { + Some("function".to_string()) + } else { + None + } + }); + + let key = key.ok_or_else(|| InvalidKeystrokeError { + keystroke: source.to_owned(), + })?; + + Ok(Keystroke { + modifiers, + key, + key_char, + }) + } + + /// Produces a representation of this key that Parse can understand. + pub fn unparse(&self) -> String { + unparse(&self.modifiers, &self.key) + } + + /// Returns true if this keystroke left + /// the ime system in an incomplete state. + pub fn is_ime_in_progress(&self) -> bool { + self.key_char.is_none() + && (is_printable_key(&self.key) || self.key.is_empty()) + && !(self.modifiers.platform + || self.modifiers.control + || self.modifiers.function + || self.modifiers.alt) + } + + /// Returns a new keystroke with the key_char filled. + /// This is used for dispatch_keystroke where we want users to + /// be able to simulate typing "space", etc. + pub fn with_simulated_ime(mut self) -> Self { + if self.key_char.is_none() + && !self.modifiers.platform + && !self.modifiers.control + && !self.modifiers.function + && !self.modifiers.alt + { + self.key_char = match self.key.as_str() { + "space" => Some(" ".into()), + "tab" => Some("\t".into()), + "enter" => Some("\n".into()), + key if !is_printable_key(key) || key.is_empty() => None, + key => { + if self.modifiers.shift { + Some(key.to_uppercase()) + } else { + Some(key.into()) + } + } + } + } + self + } +} + +impl KeybindingKeystroke { + #[cfg(target_os = "windows")] + #[expect(missing_docs)] + pub fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self { + KeybindingKeystroke { + inner, + display_modifiers, + display_key, + } + } + + /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper. + pub fn new_with_mapper( + inner: Keystroke, + use_key_equivalents: bool, + keyboard_mapper: &dyn PlatformKeyboardMapper, + ) -> Self { + keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) + } + + /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping. + pub fn from_keystroke(keystroke: Keystroke) -> Self { + #[cfg(target_os = "windows")] + { + let key = keystroke.key.clone(); + let modifiers = keystroke.modifiers; + KeybindingKeystroke { + inner: keystroke, + display_modifiers: modifiers, + display_key: key, + } + } + #[cfg(not(target_os = "windows"))] + { + KeybindingKeystroke { inner: keystroke } + } + } + + /// Returns the GPUI representation of the keystroke. + pub fn inner(&self) -> &Keystroke { + &self.inner + } + + /// Returns the modifiers. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke. + /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as + /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`. + pub fn modifiers(&self) -> &Modifiers { + #[cfg(target_os = "windows")] + { + &self.display_modifiers + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.modifiers + } + } + + /// Returns the key. + /// + /// Platform-specific behavior: + /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke. + /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`. + pub fn key(&self) -> &str { + #[cfg(target_os = "windows")] + { + &self.display_key + } + #[cfg(not(target_os = "windows"))] + { + &self.inner.key + } + } + + /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`. + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.inner.modifiers = modifiers; + #[cfg(target_os = "windows")] + { + self.display_modifiers = modifiers; + } + } + + /// Sets the key. On Windows this modifies both `inner.key` and `display_key`. + pub fn set_key(&mut self, key: String) { + #[cfg(target_os = "windows")] + { + self.display_key = key.clone(); + } + self.inner.key = key; + } + + /// Produces a representation of this key that Parse can understand. + pub fn unparse(&self) -> String { + #[cfg(target_os = "windows")] + { + unparse(&self.display_modifiers, &self.display_key) + } + #[cfg(not(target_os = "windows"))] + { + unparse(&self.inner.modifiers, &self.inner.key) + } + } + + /// Removes the key_char + pub fn remove_key_char(&mut self) { + self.inner.key_char = None; + } +} + +fn is_printable_key(key: &str) -> bool { + !matches!( + key, + "f1" | "f2" + | "f3" + | "f4" + | "f5" + | "f6" + | "f7" + | "f8" + | "f9" + | "f10" + | "f11" + | "f12" + | "f13" + | "f14" + | "f15" + | "f16" + | "f17" + | "f18" + | "f19" + | "f20" + | "f21" + | "f22" + | "f23" + | "f24" + | "f25" + | "f26" + | "f27" + | "f28" + | "f29" + | "f30" + | "f31" + | "f32" + | "f33" + | "f34" + | "f35" + | "backspace" + | "delete" + | "left" + | "right" + | "up" + | "down" + | "pageup" + | "pagedown" + | "insert" + | "home" + | "end" + | "back" + | "forward" + | "escape" + ) +} + +impl std::fmt::Display for Keystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + display_modifiers(&self.modifiers, f)?; + display_key(&self.key, f) + } +} + +impl std::fmt::Display for KeybindingKeystroke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + display_modifiers(self.modifiers(), f)?; + display_key(self.key(), f) + } +} + +/// The state of the modifier keys at some point in time +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] +pub struct Modifiers { + /// The control key + #[serde(default)] + pub control: bool, + + /// The alt key + /// Sometimes also known as the 'meta' key + #[serde(default)] + pub alt: bool, + + /// The shift key + #[serde(default)] + pub shift: bool, + + /// The command key, on macos + /// the windows key, on windows + /// the super key, on linux + #[serde(default)] + pub platform: bool, + + /// The function key + #[serde(default)] + pub function: bool, +} + +impl Modifiers { + /// Returns whether any modifier key is pressed. + pub fn modified(&self) -> bool { + self.control || self.alt || self.shift || self.platform || self.function + } + + /// Whether the semantically 'secondary' modifier key is pressed. + /// + /// On macOS, this is the command key. + /// On Linux and Windows, this is the control key. + pub fn secondary(&self) -> bool { + #[cfg(target_os = "macos")] + { + self.platform + } + + #[cfg(not(target_os = "macos"))] + { + self.control + } + } + + /// Returns how many modifier keys are pressed. + pub fn number_of_modifiers(&self) -> u8 { + self.control as u8 + + self.alt as u8 + + self.shift as u8 + + self.platform as u8 + + self.function as u8 + } + + /// Returns [`Modifiers`] with no modifiers. + pub fn none() -> Modifiers { + Default::default() + } + + /// Returns [`Modifiers`] with just the command key. + pub fn command() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// A Returns [`Modifiers`] with just the secondary key pressed. + pub fn secondary_key() -> Modifiers { + #[cfg(target_os = "macos")] + { + Modifiers { + platform: true, + ..Default::default() + } + } + + #[cfg(not(target_os = "macos"))] + { + Modifiers { + control: true, + ..Default::default() + } + } + } + + /// Returns [`Modifiers`] with just the windows key. + pub fn windows() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just the super key. + pub fn super_key() -> Modifiers { + Modifiers { + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just control. + pub fn control() -> Modifiers { + Modifiers { + control: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just alt. + pub fn alt() -> Modifiers { + Modifiers { + alt: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just shift. + pub fn shift() -> Modifiers { + Modifiers { + shift: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with just function. + pub fn function() -> Modifiers { + Modifiers { + function: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with command + shift. + pub fn command_shift() -> Modifiers { + Modifiers { + shift: true, + platform: true, + ..Default::default() + } + } + + /// Returns [`Modifiers`] with command + shift. + pub fn control_shift() -> Modifiers { + Modifiers { + shift: true, + control: true, + ..Default::default() + } + } + + /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`]. + pub fn is_subset_of(&self, other: &Modifiers) -> bool { + (*other & *self) == *self + } +} + +impl std::ops::BitOr for Modifiers { + type Output = Self; + + fn bitor(mut self, other: Self) -> Self::Output { + self |= other; + self + } +} + +impl std::ops::BitOrAssign for Modifiers { + fn bitor_assign(&mut self, other: Self) { + self.control |= other.control; + self.alt |= other.alt; + self.shift |= other.shift; + self.platform |= other.platform; + self.function |= other.function; + } +} + +impl std::ops::BitXor for Modifiers { + type Output = Self; + fn bitxor(mut self, rhs: Self) -> Self::Output { + self ^= rhs; + self + } +} + +impl std::ops::BitXorAssign for Modifiers { + fn bitxor_assign(&mut self, other: Self) { + self.control ^= other.control; + self.alt ^= other.alt; + self.shift ^= other.shift; + self.platform ^= other.platform; + self.function ^= other.function; + } +} + +impl std::ops::BitAnd for Modifiers { + type Output = Self; + fn bitand(mut self, rhs: Self) -> Self::Output { + self &= rhs; + self + } +} + +impl std::ops::BitAndAssign for Modifiers { + fn bitand_assign(&mut self, other: Self) { + self.control &= other.control; + self.alt &= other.alt; + self.shift &= other.shift; + self.platform &= other.platform; + self.function &= other.function; + } +} + +/// The state of the capslock key at some point in time +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)] +pub struct Capslock { + /// The capslock key is on + #[serde(default)] + pub on: bool, +} + +impl AsKeystroke for Keystroke { + fn as_keystroke(&self) -> &Keystroke { + self + } +} + +impl AsKeystroke for KeybindingKeystroke { + fn as_keystroke(&self) -> &Keystroke { + &self.inner + } +} + +impl AsKeystroke for &T { + fn as_keystroke(&self) -> &Keystroke { + (**self).as_keystroke() + } +} + +fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + Ok(()) +} + +fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let key = match key { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) +} + +#[inline] +fn unparse(modifiers: &Modifiers, key: &str) -> String { + let mut result = String::new(); + if modifiers.function { + result.push_str("fn-"); + } + if modifiers.control { + result.push_str("ctrl-"); + } + if modifiers.alt { + result.push_str("alt-"); + } + if modifiers.platform { + #[cfg(target_os = "macos")] + result.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + result.push_str("super-"); + + #[cfg(target_os = "windows")] + result.push_str("win-"); + } + if modifiers.shift { + result.push_str("shift-"); + } + result.push_str(&key); + result +} diff --git a/vendor/gpui-pre/src/platform/layer_shell.rs b/vendor/gpui-pre/src/platform/layer_shell.rs new file mode 100644 index 0000000..8be1b5f --- /dev/null +++ b/vendor/gpui-pre/src/platform/layer_shell.rs @@ -0,0 +1,83 @@ +use bitflags::bitflags; +use thiserror::Error; + +use crate::Pixels; + +/// The layer the surface is rendered on. Multiple surfaces can share a layer, and ordering within +/// a single layer is undefined. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum Layer { + /// The background layer, typically used for wallpapers. + Background, + + /// The bottom layer. + Bottom, + + /// The top layer, typically used for fullscreen windows. + Top, + + /// The overlay layer, used for surfaces that should always be on top. + #[default] + Overlay, +} + +bitflags! { + /// Screen anchor point for layer_shell surfaces. These can be used in any combination, e.g. + /// specifying `Anchor::LEFT | Anchor::RIGHT` will stretch the surface across the width of the + /// screen. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] + pub struct Anchor: u32 { + /// Anchor to the top edge of the screen. + const TOP = 1; + /// Anchor to the bottom edge of the screen. + const BOTTOM = 2; + /// Anchor to the left edge of the screen. + const LEFT = 4; + /// Anchor to the right edge of the screen. + const RIGHT = 8; + } +} + +/// Keyboard interactivity mode for the layer_shell surfaces. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum KeyboardInteractivity { + /// No keyboard inputs will be delivered to the surface and it won't be able to receive + /// keyboard focus. + None, + + /// The surface will receive exclusive keyboard focus as long as it is above the shell surface + /// layer, and no other layer_shell surfaces are above it. + Exclusive, + + /// The surface can be focused similarly to a normal window. + #[default] + OnDemand, +} + +/// Options for creating a layer_shell window. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LayerShellOptions { + /// The namespace for the surface, mostly used by compositors to apply rules, can not be + /// changed after the surface is created. + pub namespace: String, + /// The layer the surface is rendered on. + pub layer: Layer, + /// The anchor point of the surface. + pub anchor: Anchor, + /// Requests that the compositor avoids occluding an area with other surfaces. + pub exclusive_zone: Option, + /// The anchor point of the exclusive zone, will be determined using the anchor if left + /// unspecified. + pub exclusive_edge: Option, + /// Margins between the surface and its anchor point(s). + /// Specified in CSS order: top, right, bottom, left. + pub margin: Option<(Pixels, Pixels, Pixels, Pixels)>, + /// How keyboard events should be delivered to the surface. + pub keyboard_interactivity: KeyboardInteractivity, +} + +/// An error indicating that an action failed because the compositor doesn't support the required +/// layer_shell protocol. +#[derive(Debug, Error)] +#[error("Compositor doesn't support zwlr_layer_shell_v1")] +pub struct LayerShellNotSupportedError; diff --git a/vendor/gpui-pre/src/platform/popup.rs b/vendor/gpui-pre/src/platform/popup.rs new file mode 100644 index 0000000..a1f8d7e --- /dev/null +++ b/vendor/gpui-pre/src/platform/popup.rs @@ -0,0 +1,134 @@ +use bitflags::bitflags; +use thiserror::Error; + +use crate::{AnyWindowHandle, Bounds, Pixels, Point}; + +/// Options for a parent-anchored popup window such as a menu, dropdown, context menu or tooltip. +/// +/// A popup is placed relative to an anchor rectangle on its parent window rather than at an +/// absolute screen position. The platform resolves the final position, so this works both on +/// systems where the compositor owns window placement (Wayland) and on platforms with absolute +/// coordinates. +/// +/// The popup's size comes from [`WindowOptions::window_bounds`](crate::WindowOptions), whose +/// origin is ignored. All coordinates are in logical pixels. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PopupOptions { + /// The window the popup is anchored to. + pub parent: AnyWindowHandle, + + /// The rectangle the popup is positioned relative to, in the parent window's coordinate + /// space (the same space element bounds are in). For example, a dropdown menu uses the + /// bounds of the button that opened it. + pub anchor_rect: Bounds, + + /// Which point of [`Self::anchor_rect`] the popup is anchored to. + pub anchor: PopupAnchor, + + /// The direction in which the popup extends away from the anchor point. A dropdown that + /// drops below its button anchors to [`PopupAnchor::BottomLeft`] with a gravity of + /// [`PopupGravity::BottomRight`] so it grows down and to the right. + pub gravity: PopupGravity, + + /// How the platform may adjust the popup if the requested placement would put it off-screen. + pub constraint_adjustment: PopupConstraintAdjustment, + + /// An additional offset applied to the popup after anchoring. + pub offset: Point, + + /// Whether the popup should take an explicit input grab. + /// + /// Grabbing popups behave like menus: they take keyboard focus and are dismissed when the + /// user clicks outside of them or presses a dismissing key. Use it for menus and comboboxes, + /// not for tooltips or other passive popups. + /// + /// A grab must be requested while the triggering input is still active, in practice the + /// press of the mouse button that opens the popup. Open grabbing popups from a mouse-down + /// handler rather than a click handler, otherwise the grab is refused. + /// + /// Automatic dismissal only covers input aimed at other applications. A click elsewhere in + /// your own application still reaches it as usual, so closing the popup in that case is up + /// to you. Nested grabbing popups must be closed in the reverse order they were opened. + pub grab: bool, +} + +/// The point of the anchor rectangle that a popup is anchored to. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum PopupAnchor { + /// Anchor to the center of the anchor rectangle. + #[default] + Center, + /// Anchor to the center of the top edge. + Top, + /// Anchor to the center of the bottom edge. + Bottom, + /// Anchor to the center of the left edge. + Left, + /// Anchor to the center of the right edge. + Right, + /// Anchor to the top-left corner. + TopLeft, + /// Anchor to the bottom-left corner. + BottomLeft, + /// Anchor to the top-right corner. + TopRight, + /// Anchor to the bottom-right corner. + BottomRight, +} + +/// The direction in which a popup extends away from its anchor point. +/// +/// For instance, a gravity of [`PopupGravity::BottomRight`] places the popup below and to the +/// right of the anchor point. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum PopupGravity { + /// The popup is centered over the anchor point. + #[default] + Center, + /// The popup extends upwards from the anchor point. + Top, + /// The popup extends downwards from the anchor point. + Bottom, + /// The popup extends to the left of the anchor point. + Left, + /// The popup extends to the right of the anchor point. + Right, + /// The popup extends up and to the left of the anchor point. + TopLeft, + /// The popup extends down and to the left of the anchor point. + BottomLeft, + /// The popup extends up and to the right of the anchor point. + TopRight, + /// The popup extends down and to the right of the anchor point. + BottomRight, +} + +bitflags! { + /// How a popup may be adjusted by the platform if the requested placement would put it + /// off-screen. If no flags are set, the popup is placed exactly as requested and may be + /// clipped. + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] + pub struct PopupConstraintAdjustment: u32 { + /// The popup may be slid horizontally to stay on-screen. + const SLIDE_X = 1; + /// The popup may be slid vertically to stay on-screen. + const SLIDE_Y = 2; + /// The popup's anchor and gravity may be flipped horizontally to stay on-screen. + const FLIP_X = 4; + /// The popup's anchor and gravity may be flipped vertically to stay on-screen. + const FLIP_Y = 8; + /// The popup may be shrunk horizontally to stay on-screen. + const RESIZE_X = 16; + /// The popup may be shrunk vertically to stay on-screen. + const RESIZE_Y = 32; + } +} + +/// Returned when the current platform has no native popup implementation yet. +/// +/// Native popups are separate from gpui's in-window popovers, which are drawn as elements inside +/// an existing window. A caller that wants a popup on every platform should treat this error as +/// a cue to fall back to that in-window rendering. +#[derive(Debug, Error)] +#[error("popups are not supported on this platform")] +pub struct PopupNotSupportedError; diff --git a/vendor/gpui-pre/src/platform/scap_screen_capture.rs b/vendor/gpui-pre/src/platform/scap_screen_capture.rs new file mode 100644 index 0000000..797e19b --- /dev/null +++ b/vendor/gpui-pre/src/platform/scap_screen_capture.rs @@ -0,0 +1,325 @@ +//! Screen capture for Linux and Windows +use crate::{ + DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + Size, SourceMetadata, size, +}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use scap::Target; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool}; + +/// Populates the receiver with the screens that can be captured. +/// +/// `scap_default_target_source` should be used instead on Wayland, since `scap_screen_sources` +/// won't return any results. +#[allow(dead_code)] +pub fn scap_screen_sources( + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + get_screen_targets(sources_tx); + to_dyn_screen_capture_sources(sources_rx, foreground_executor) +} + +/// Starts screen capture for the default target, and populates the receiver with a single source +/// for it. The first frame of the screen capture is used to determine the size of the stream. +/// +/// On Wayland (Linux), prompts the user to select a target, and populates the receiver with a +/// single screen capture source for their selection. +#[allow(dead_code)] +pub(crate) fn start_scap_default_target_source( + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (sources_tx, sources_rx) = oneshot::channel(); + start_default_target_screen_capture(sources_tx); + to_dyn_screen_capture_sources(sources_rx, foreground_executor) +} + +struct ScapCaptureSource { + target: scap::Display, + size: Size, +} + +/// Populates the sender with the screens available for capture. +fn get_screen_targets(sources_tx: oneshot::Sender>>) { + // Due to use of blocking APIs, a new thread is used. + std::thread::spawn(|| { + let targets = match scap::get_all_targets() { + Ok(targets) => targets, + Err(err) => { + sources_tx.send(Err(err)).ok(); + return; + } + }; + let sources = targets + .into_iter() + .filter_map(|target| match target { + scap::Target::Display(display) => { + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; + Some(ScapCaptureSource { + target: display, + size, + }) + } + scap::Target::Window(_) => None, + }) + .collect::>(); + sources_tx.send(Ok(sources)).ok(); + }); +} + +impl ScreenCaptureSource for ScapCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.target.title.clone().into()), + is_main: None, + id: self.target.id as u64, + }) + } + + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + let (stream_tx, stream_rx) = oneshot::channel(); + let target = self.target.clone(); + + // Due to use of blocking APIs, a dedicated thread is used. + std::thread::spawn(move || { + match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { + Ok(mut capturer) => { + capturer.start_capture(); + run_capture(capturer, target.clone(), frame_callback, stream_tx); + } + Err(e) => { + stream_tx.send(Err(e)).ok(); + } + } + }); + + to_dyn_screen_capture_stream(stream_rx, foreground_executor) + } +} + +struct ScapDefaultTargetCaptureSource { + // Sender populated by single call to `ScreenCaptureSource::stream`. + stream_call_tx: std::sync::mpsc::SyncSender<( + // Provides the result of `ScreenCaptureSource::stream`. + oneshot::Sender>, + // Callback for frames. + Box, + )>, + target: scap::Display, + size: Size, +} + +/// Starts screen capture on the default capture target, and populates the sender with the source. +fn start_default_target_screen_capture( + sources_tx: oneshot::Sender>>, +) { + // Due to use of blocking APIs, a dedicated thread is used. + std::thread::spawn(|| { + let start_result = gpui_util::maybe!({ + let mut capturer = new_scap_capturer(None)?; + capturer.start_capture(); + let first_frame = capturer + .get_next_frame() + .context("Failed to get first frame of screenshare to get the size.")?; + let size = frame_size(&first_frame); + let target = capturer + .target() + .context("Unable to determine the target display.")?; + let target = target.clone(); + Ok((capturer, size, target)) + }); + + match start_result { + Ok((capturer, size, Target::Display(display))) => { + let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); + sources_tx + .send(Ok(vec![ScapDefaultTargetCaptureSource { + stream_call_tx, + size, + target: display.clone(), + }])) + .ok(); + let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { + return; + }; + run_capture(capturer, display, frame_callback, stream_tx); + } + Err(e) => { + sources_tx.send(Err(e)).ok(); + } + _ => { + sources_tx + .send(Err(anyhow!("The screen capture source is not a display"))) + .ok(); + } + } + }); +} + +impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: None, + is_main: None, + id: self.target.id as u64, + }) + } + + fn stream( + &self, + foreground_executor: &ForegroundExecutor, + frame_callback: Box, + ) -> oneshot::Receiver>> { + let (tx, rx) = oneshot::channel(); + match self.stream_call_tx.try_send((tx, frame_callback)) { + Ok(()) => {} + Err(std::sync::mpsc::TrySendError::Full((tx, _))) + | Err(std::sync::mpsc::TrySendError::Disconnected((tx, _))) => { + // Note: support could be added for being called again after end of prior stream. + tx.send(Err(anyhow!( + "Can't call ScapDefaultTargetCaptureSource::stream multiple times." + ))) + .ok(); + } + } + to_dyn_screen_capture_stream(rx, foreground_executor) + } +} + +fn new_scap_capturer(target: Option) -> Result { + scap::capturer::Capturer::build(scap::capturer::Options { + fps: 60, + show_cursor: true, + show_highlight: true, + // Note that the actual frame output type may differ. + output_type: scap::frame::FrameType::YUVFrame, + output_resolution: scap::capturer::Resolution::Captured, + crop_area: None, + target, + excluded_targets: None, + }) +} + +fn run_capture( + mut capturer: scap::capturer::Capturer, + display: scap::Display, + frame_callback: Box, + stream_tx: oneshot::Sender>, +) { + let cancel_stream = Arc::new(AtomicBool::new(false)); + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; + let stream_send_result = stream_tx.send(Ok(ScapStream { + cancel_stream: cancel_stream.clone(), + display, + size, + })); + if stream_send_result.is_err() { + return; + } + while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { + match capturer.get_next_frame() { + Ok(frame) => frame_callback(ScreenCaptureFrame(frame)), + Err(err) => { + log::error!("Halting screen capture due to error: {err}"); + break; + } + } + } + capturer.stop_capture(); +} + +struct ScapStream { + cancel_stream: Arc, + display: scap::Display, + size: Size, +} + +impl ScreenCaptureStream for ScapStream { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.display.title.clone().into()), + is_main: None, + id: self.display.id as u64, + }) + } +} + +impl Drop for ScapStream { + fn drop(&mut self) { + self.cancel_stream.store(true, atomic::Ordering::SeqCst); + } +} + +fn frame_size(frame: &scap::frame::Frame) -> Size { + let (width, height) = match frame { + scap::frame::Frame::YUVFrame(frame) => (frame.width, frame.height), + scap::frame::Frame::RGB(frame) => (frame.width, frame.height), + scap::frame::Frame::RGBx(frame) => (frame.width, frame.height), + scap::frame::Frame::XBGR(frame) => (frame.width, frame.height), + scap::frame::Frame::BGRx(frame) => (frame.width, frame.height), + scap::frame::Frame::BGR0(frame) => (frame.width, frame.height), + scap::frame::Frame::BGRA(frame) => (frame.width, frame.height), + }; + size(DevicePixels(width), DevicePixels(height)) +} + +/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their +/// results into `Rc`. They need to `Send` their capture source, and so +/// the capture source structs are used as `Rc` is not `Send`. +fn to_dyn_screen_capture_sources( + sources_rx: oneshot::Receiver>>, + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>>> { + let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); + foreground_executor + .spawn(async move { + match sources_rx.await { + Ok(Ok(results)) => dyn_sources_tx + .send(Ok(results + .into_iter() + .map(|source| Rc::new(source) as Rc) + .collect::>())) + .ok(), + Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), + Err(oneshot::Canceled) => None, + } + }) + .detach(); + dyn_sources_rx +} + +/// Same motivation as `to_dyn_screen_capture_sources` above. +fn to_dyn_screen_capture_stream( + sources_rx: oneshot::Receiver>, + foreground_executor: &ForegroundExecutor, +) -> oneshot::Receiver>> { + let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); + foreground_executor + .spawn(async move { + match sources_rx.await { + Ok(Ok(stream)) => dyn_sources_tx + .send(Ok(Box::new(stream) as Box)) + .ok(), + Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), + Err(oneshot::Canceled) => None, + } + }) + .detach(); + dyn_sources_rx +} diff --git a/vendor/gpui-pre/src/platform/test.rs b/vendor/gpui-pre/src/platform/test.rs new file mode 100644 index 0000000..a327d8f --- /dev/null +++ b/vendor/gpui-pre/src/platform/test.rs @@ -0,0 +1,12 @@ +mod dispatcher; +mod display; +mod platform; +mod window; + +pub use dispatcher::*; +pub(crate) use display::*; +pub(crate) use platform::*; +pub(crate) use window::*; + +#[cfg(any(test, feature = "test-support"))] +pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/vendor/gpui-pre/src/platform/test/dispatcher.rs b/vendor/gpui-pre/src/platform/test/dispatcher.rs new file mode 100644 index 0000000..c48e491 --- /dev/null +++ b/vendor/gpui-pre/src/platform/test/dispatcher.rs @@ -0,0 +1,148 @@ +use crate::{PlatformDispatcher, Priority, RunnableVariant}; +use scheduler::Instant; +use scheduler::{Clock, Scheduler, SessionId, TestScheduler, TestSchedulerConfig, Yield}; +use std::{ + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, +}; + +/// TestDispatcher provides deterministic async execution for tests. +/// +/// This implementation delegates task scheduling to the scheduler crate's `TestScheduler`. +/// Access the scheduler directly via `scheduler()` for clock, rng, and parking control. +#[doc(hidden)] +pub struct TestDispatcher { + session_id: SessionId, + scheduler: Arc, + num_cpus_override: Arc, +} + +impl TestDispatcher { + pub fn new(seed: u64) -> Self { + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig { + seed, + randomize_order: true, + allow_parking: false, + capture_pending_traces: std::env::var("PENDING_TRACES") + .map_or(false, |var| var == "1" || var == "true"), + timeout_ticks: 0..=1000, + })); + Self::from_scheduler(scheduler) + } + + pub fn from_scheduler(scheduler: Arc) -> Self { + TestDispatcher { + session_id: scheduler.allocate_session_id(), + scheduler, + num_cpus_override: Arc::new(AtomicUsize::new(0)), + } + } + + pub fn scheduler(&self) -> &Arc { + &self.scheduler + } + + pub fn session_id(&self) -> SessionId { + self.session_id + } + + pub fn drain_tasks(&self) { + self.scheduler.drain_tasks(); + } + + pub fn advance_clock(&self, by: Duration) { + self.scheduler.advance_clock(by); + } + + pub fn advance_clock_to_next_timer(&self) -> bool { + self.scheduler.advance_clock_to_next_timer() + } + + pub fn simulate_random_delay(&self) -> Yield { + self.scheduler.yield_random() + } + + pub fn tick(&self, background_only: bool) -> bool { + if background_only { + self.scheduler.tick_background_only() + } else { + self.scheduler.tick() + } + } + + pub fn run_until_parked(&self) { + while self.tick(false) {} + } + + pub fn allow_parking(&self) { + self.scheduler.allow_parking(); + } + + pub fn forbid_parking(&self) { + self.scheduler.forbid_parking(); + } + + /// Override the value returned by `BackgroundExecutor::num_cpus()` in tests. + /// A value of 0 means no override (the default of 4 is used). + pub fn set_num_cpus(&self, count: usize) { + self.num_cpus_override.store(count, Ordering::SeqCst); + } + + /// Returns the overridden CPU count, or `None` if no override is set. + pub fn num_cpus_override(&self) -> Option { + match self.num_cpus_override.load(Ordering::SeqCst) { + 0 => None, + n => Some(n), + } + } +} + +impl Clone for TestDispatcher { + fn clone(&self) -> Self { + let session_id = self.scheduler.allocate_session_id(); + Self { + session_id, + scheduler: self.scheduler.clone(), + num_cpus_override: self.num_cpus_override.clone(), + } + } +} + +impl PlatformDispatcher for TestDispatcher { + fn is_main_thread(&self) -> bool { + self.scheduler.is_main_thread() + } + + fn now(&self) -> Instant { + self.scheduler.clock().now() + } + + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + self.scheduler + .schedule_background_with_priority(runnable, priority); + } + + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) { + self.scheduler.schedule_local(self.session_id, runnable); + } + + fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) { + panic!( + "dispatch_after should not be called in tests. \ + Use BackgroundExecutor::timer() which uses the scheduler's native timer." + ); + } + + fn as_test(&self) -> Option<&TestDispatcher> { + Some(self) + } + + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + f(); + }); + } +} diff --git a/vendor/gpui-pre/src/platform/test/display.rs b/vendor/gpui-pre/src/platform/test/display.rs new file mode 100644 index 0000000..c4adb01 --- /dev/null +++ b/vendor/gpui-pre/src/platform/test/display.rs @@ -0,0 +1,33 @@ +use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, px}; +use anyhow::{Ok, Result}; + +#[derive(Debug)] +pub(crate) struct TestDisplay { + id: DisplayId, + uuid: uuid::Uuid, + bounds: Bounds, +} + +impl TestDisplay { + pub fn new() -> Self { + TestDisplay { + id: DisplayId(1), + uuid: uuid::Uuid::new_v4(), + bounds: Bounds::from_corners(Point::default(), Point::new(px(1920.), px(1080.))), + } + } +} + +impl PlatformDisplay for TestDisplay { + fn id(&self) -> crate::DisplayId { + self.id + } + + fn uuid(&self) -> Result { + Ok(self.uuid) + } + + fn bounds(&self) -> crate::Bounds { + self.bounds + } +} diff --git a/vendor/gpui-pre/src/platform/test/platform.rs b/vendor/gpui-pre/src/platform/test/platform.rs new file mode 100644 index 0000000..4bf0838 --- /dev/null +++ b/vendor/gpui-pre/src/platform/test/platform.rs @@ -0,0 +1,742 @@ +#[cfg(any(test, feature = "test-support"))] +use crate::NoopTextSystem; +#[cfg(any(test, feature = "test-support"))] +use crate::PathPromptOptions; +use crate::{ + ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, + DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay, + PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString, + SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow, + ThermalState, WindowAppearance, WindowParams, size, +}; +use anyhow::Result; +#[cfg(any(test, feature = "test-support"))] +use collections::VecDeque; +use futures::channel::oneshot; +use parking_lot::Mutex; +use std::{ + cell::{Cell, RefCell}, + path::{Path, PathBuf}, + rc::{Rc, Weak}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, +}; + +/// TestPlatform implements the Platform trait for use in tests. +pub(crate) struct TestPlatform { + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + + pub(crate) active_window: RefCell>, + active_display: Rc, + active_cursor: Mutex, + current_clipboard_item: Mutex>, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + current_primary_item: Mutex>, + #[cfg(target_os = "macos")] + current_find_pasteboard_item: Mutex>, + #[cfg(any(test, feature = "test-support"))] + pub(crate) prompts: RefCell, + screen_capture_sources: RefCell>, + pub opened_url: RefCell>, + pub(crate) system_notifications: RefCell, + pub text_system: Arc, + pub expect_restart: + RefCell, Vec)>>>, + idle_sleep_prevention_count: Arc, + idle_sleep_prevention_delay: Cell, + idle_sleep_prevention_fails: Cell, + headless_renderer_factory: + Option anyhow::Result>>>>, + weak: Weak, + menus: RefCell>, +} + +#[derive(Clone)] +/// A fake screen capture source, used for testing. +pub struct TestScreenCaptureSource {} + +/// A fake screen capture stream, used for testing. +pub struct TestScreenCaptureStream {} + +impl ScreenCaptureSource for TestScreenCaptureSource { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + id: 0, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) + } + + fn stream( + &self, + _foreground_executor: &ForegroundExecutor, + _frame_callback: Box, + ) -> oneshot::Receiver>> { + let (mut tx, rx) = oneshot::channel(); + let stream = TestScreenCaptureStream {}; + tx.send(Ok(Box::new(stream) as Box)) + .ok(); + rx + } +} + +impl ScreenCaptureStream for TestScreenCaptureStream { + fn metadata(&self) -> Result { + TestScreenCaptureSource {}.metadata() + } +} + +#[cfg(any(test, feature = "test-support"))] +struct TestPrompt { + msg: String, + detail: Option, + answers: Vec, + tx: oneshot::Sender, +} + +#[derive(Default)] +pub(crate) struct TestSystemNotifications { + pub(crate) app_identity: Option<(SharedString, SharedString)>, + pub(crate) shown: Vec, + pub(crate) delivered: Vec, + pub(crate) dismissed: Vec, + response_callback: Option>, +} + +#[cfg(any(test, feature = "test-support"))] +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque, + new_path: VecDeque<(PathBuf, oneshot::Sender>>)>, + paths: VecDeque<( + PathPromptOptions, + oneshot::Sender>>>, + )>, +} + +impl TestPlatform { + #[cfg(any(test, feature = "test-support"))] + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + Self::with_platform( + executor, + foreground_executor, + Arc::new(NoopTextSystem), + None, + ) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn with_text_system( + executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + ) -> Rc { + Self::with_platform(executor, foreground_executor, text_system, None) + } + + pub fn with_platform( + executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + text_system: Arc, + headless_renderer_factory: Option< + Box anyhow::Result>>>, + >, + ) -> Rc { + Rc::new_cyclic(|weak| TestPlatform { + background_executor: executor, + foreground_executor, + #[cfg(any(test, feature = "test-support"))] + prompts: Default::default(), + screen_capture_sources: Default::default(), + active_cursor: Default::default(), + active_display: Rc::new(TestDisplay::new()), + active_window: Default::default(), + expect_restart: Default::default(), + current_clipboard_item: Mutex::new(None), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + current_primary_item: Mutex::new(None), + #[cfg(target_os = "macos")] + current_find_pasteboard_item: Mutex::new(None), + idle_sleep_prevention_count: Arc::new(AtomicUsize::new(0)), + idle_sleep_prevention_delay: Cell::new(Duration::ZERO), + idle_sleep_prevention_fails: Cell::new(false), + weak: weak.clone(), + opened_url: Default::default(), + system_notifications: Default::default(), + text_system, + headless_renderer_factory, + menus: Default::default(), + }) + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + let (path, tx) = self + .prompts + .borrow_mut() + .new_path + .pop_front() + .expect("no pending new path prompt"); + tx.send(Ok(select_path(&path))).ok(); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn simulate_path_prompt_response( + &self, + select_paths: impl FnOnce(&PathPromptOptions) -> Option>, + ) { + let (options, tx) = self + .prompts + .borrow_mut() + .paths + .pop_front() + .expect("no pending paths prompt"); + let selection = select_paths(&options); + if let Some(paths) = &selection + && !options.multiple + && paths.len() > 1 + { + panic!( + "selected {} paths for a prompt that does not allow multiple selection", + paths.len() + ); + } + tx.send(Ok(selection)).ok(); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn did_prompt_for_paths(&self) -> bool { + !self.prompts.borrow().paths.is_empty() + } + + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub(crate) fn simulate_prompt_answer(&self, response: &str) { + let prompt = self + .prompts + .borrow_mut() + .multiple_choice + .pop_front() + .expect("no pending multiple choice prompt"); + let Some(ix) = prompt.answers.iter().position(|a| a == response) else { + panic!( + "PROMPT: {}\n{:?}\n{:?}\nCannot respond with {}", + prompt.msg, prompt.detail, prompt.answers, response + ) + }; + prompt.tx.send(ix).ok(); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn pending_prompt(&self) -> Option<(String, String)> { + let prompts = self.prompts.borrow(); + let prompt = prompts.multiple_choice.front()?; + Some(( + prompt.msg.clone(), + prompt.detail.clone().unwrap_or_default(), + )) + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { + *self.screen_capture_sources.borrow_mut() = sources; + } + + /// Queues the prompt so a test can later inspect or answer it through + /// [`Self::pending_prompt`] and [`Self::simulate_prompt_answer`]. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn prompt( + &self, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + let answers: Vec = answers.iter().map(|s| s.label().to_string()).collect(); + self.prompts + .borrow_mut() + .multiple_choice + .push_back(TestPrompt { + msg: msg.to_string(), + detail: detail.map(|s| s.to_string()), + answers, + tx, + }); + rx + } + + /// Benchmarks have no API to answer a prompt, so this doesn't retain it + /// for later inspection; dropping the sender immediately cancels the + /// returned receiver instead of leaving it pending indefinitely. + #[cfg(not(any(test, feature = "test-support")))] + pub(crate) fn prompt( + &self, + _msg: &str, + _detail: Option<&str>, + _answers: &[PromptButton], + ) -> oneshot::Receiver { + oneshot::channel().1 + } + + pub(crate) fn set_active_window(&self, window: Option) { + let executor = self.foreground_executor(); + let previous_window = self.active_window.borrow_mut().take(); + self.active_window.borrow_mut().clone_from(&window); + + executor + .spawn(async move { + if let Some(previous_window) = previous_window { + if let Some(window) = window.as_ref() + && Rc::ptr_eq(&previous_window.0, &window.0) + { + return; + } + previous_window.simulate_active_status_change(false); + } + if let Some(window) = window { + window.simulate_active_status_change(true); + } + }) + .detach(); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + !self.prompts.borrow().new_path.is_empty() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn active_idle_sleep_preventions(&self) -> usize { + self.idle_sleep_prevention_count.load(Ordering::SeqCst) + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn set_idle_sleep_prevention_delay(&self, delay: Duration) { + self.idle_sleep_prevention_delay.set(delay); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn set_idle_sleep_prevention_fails(&self, fails: bool) { + self.idle_sleep_prevention_fails.set(fails); + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn app_identity(&self) -> Option<(SharedString, SharedString)> { + self.system_notifications.borrow().app_identity.clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn shown_system_notifications(&self) -> Vec { + self.system_notifications.borrow().shown.clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn delivered_system_notifications(&self) -> Vec { + self.system_notifications.borrow().delivered.clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn dismissed_system_notifications(&self) -> Vec { + self.system_notifications.borrow().dismissed.clone() + } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn simulate_system_notification_response( + &self, + response: SystemNotificationResponse, + ) { + let callback = self + .system_notifications + .borrow_mut() + .response_callback + .take(); + if let Some(mut callback) = callback { + callback(response); + self.system_notifications + .borrow_mut() + .response_callback + .get_or_insert(callback); + } + } +} + +impl Platform for TestPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.text_system.clone() + } + + fn keyboard_layout(&self) -> Box { + Box::new(TestKeyboardLayout) + } + + fn keyboard_mapper(&self) -> Rc { + Rc::new(DummyKeyboardMapper) + } + + fn on_keyboard_layout_change(&self, _: Box) {} + + fn on_thermal_state_change(&self, _: Box) {} + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + + fn prevent_idle_sleep(&self, reason: &str) -> Task> { + let count = self.idle_sleep_prevention_count.clone(); + let fails = self.idle_sleep_prevention_fails.get(); + let reason = reason.to_owned(); + let acquire = move || { + if fails { + anyhow::bail!("Idle sleep prevention for {reason:?} is set to fail in this test"); + } + count.fetch_add(1, Ordering::SeqCst); + Ok(ActivityGuard::new(move || { + count.fetch_sub(1, Ordering::SeqCst); + })) + }; + + let delay = self.idle_sleep_prevention_delay.get(); + if delay.is_zero() { + Task::ready(acquire()) + } else { + let delay = self.background_executor.timer(delay); + self.foreground_executor.spawn(async move { + delay.await; + acquire() + }) + } + } + + fn run(&self, _on_finish_launching: Box) { + unimplemented!() + } + + fn quit(&self) {} + + fn restart(&self, path: Option, arguments: Vec) { + if let Some(tx) = self.expect_restart.take() { + tx.send((path, arguments)).unwrap(); + } + } + + fn activate(&self, _ignoring_other_apps: bool) { + // + } + + fn hide(&self) { + unimplemented!() + } + + fn hide_other_apps(&self) { + unimplemented!() + } + + fn unhide_other_apps(&self) { + unimplemented!() + } + + fn displays(&self) -> Vec> { + vec![self.active_display.clone()] + } + + fn primary_display(&self) -> Option> { + Some(self.active_display.clone()) + } + + fn is_screen_capture_supported(&self) -> bool { + true + } + + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (mut tx, rx) = oneshot::channel(); + tx.send(Ok(self + .screen_capture_sources + .borrow() + .iter() + .map(|source| Rc::new(source.clone()) as Rc) + .collect())) + .ok(); + rx + } + + fn active_window(&self) -> Option { + self.active_window + .borrow() + .as_ref() + .map(|window| window.0.lock().handle) + } + + fn open_window( + &self, + handle: AnyWindowHandle, + params: WindowParams, + ) -> anyhow::Result> { + let renderer = match self.headless_renderer_factory.as_ref() { + Some(factory) => factory()?, + None => None, + }; + let window = TestWindow::new( + handle, + params, + self.weak.clone(), + self.active_display.clone(), + renderer, + ); + Ok(Box::new(window)) + } + + fn window_appearance(&self) -> WindowAppearance { + WindowAppearance::Light + } + + fn open_url(&self, url: &str) { + *self.opened_url.borrow_mut() = Some(url.to_string()) + } + + fn on_open_urls(&self, _callback: Box)>) { + unimplemented!() + } + + /// Queues the prompt so a test can later answer it through + /// [`Self::simulate_path_prompt_response`]. + #[cfg(any(test, feature = "test-support"))] + fn prompt_for_paths( + &self, + options: crate::PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (tx, rx) = oneshot::channel(); + self.prompts.borrow_mut().paths.push_back((options, tx)); + rx + } + + /// Benchmarks have no API to answer a path prompt, so this doesn't + /// retain it for later inspection; dropping the sender immediately + /// cancels the returned receiver instead of leaving it pending + /// indefinitely. + #[cfg(not(any(test, feature = "test-support")))] + fn prompt_for_paths( + &self, + _options: crate::PathPromptOptions, + ) -> oneshot::Receiver>>> { + oneshot::channel().1 + } + + /// Queues the prompt so a test can later answer it through + /// [`Self::simulate_new_path_selection`]. + #[cfg(any(test, feature = "test-support"))] + fn prompt_for_new_path( + &self, + directory: &std::path::Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (tx, rx) = oneshot::channel(); + self.prompts + .borrow_mut() + .new_path + .push_back((directory.to_path_buf(), tx)); + rx + } + + /// Benchmarks have no API to answer a new-path prompt, so this doesn't + /// retain it for later inspection; dropping the sender immediately + /// cancels the returned receiver instead of leaving it pending + /// indefinitely. + #[cfg(not(any(test, feature = "test-support")))] + fn prompt_for_new_path( + &self, + _directory: &std::path::Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + oneshot::channel().1 + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + + fn reveal_path(&self, _path: &std::path::Path) { + unimplemented!() + } + + fn on_quit(&self, _callback: Box bool>) {} + + fn on_reopen(&self, _callback: Box) { + unimplemented!() + } + + fn on_system_sleep(&self, _callback: Box) {} + + fn on_system_wake(&self, _callback: Box) {} + + fn set_app_identity(&self, identifier: &str, name: &str) { + self.system_notifications.borrow_mut().app_identity = + Some((identifier.to_string().into(), name.to_string().into())); + } + + fn show_system_notification(&self, notification: SystemNotification) { + let mut system_notifications = self.system_notifications.borrow_mut(); + if system_notifications.app_identity.is_none() { + return; + } + + let delivered = system_notifications + .delivered + .iter_mut() + .find(|delivered| delivered.tag == notification.tag); + if let Some(delivered) = delivered { + *delivered = notification.clone(); + } else { + system_notifications.delivered.push(notification.clone()); + } + system_notifications.shown.push(notification); + } + + fn dismiss_system_notification(&self, tag: &str) { + let mut system_notifications = self.system_notifications.borrow_mut(); + system_notifications + .delivered + .retain(|notification| notification.tag != tag); + system_notifications + .dismissed + .push(SharedString::from(tag.to_string())); + } + + fn on_system_notification_response( + &self, + callback: Box, + ) { + self.system_notifications.borrow_mut().response_callback = Some(callback); + } + + fn set_menus(&self, menus: Vec, _keymap: &Keymap) { + *self.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect() + } + + fn get_menus(&self) -> Option> { + Some(self.menus.borrow().clone()) + } + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + + fn add_recent_document(&self, _paths: &Path) {} + + fn on_app_menu_action(&self, _callback: Box) {} + + fn on_will_open_app_menu(&self, _callback: Box) {} + + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + + fn app_path(&self) -> Result { + unimplemented!() + } + + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { + unimplemented!() + } + + fn set_cursor_style(&self, style: crate::CursorStyle) { + *self.active_cursor.lock() = style; + } + + fn hide_cursor_until_mouse_moves(&self) {} + + fn is_cursor_visible(&self) -> bool { + true + } + + fn should_auto_hide_scrollbars(&self) -> bool { + false + } + + fn read_from_clipboard(&self) -> Option { + self.current_clipboard_item.lock().clone() + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.current_clipboard_item.lock() = Some(item); + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn read_from_primary(&self) -> Option { + self.current_primary_item.lock().clone() + } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + fn write_to_primary(&self, item: ClipboardItem) { + *self.current_primary_item.lock() = Some(item); + } + + #[cfg(target_os = "macos")] + fn read_from_find_pasteboard(&self) -> Option { + self.current_find_pasteboard_item.lock().clone() + } + + #[cfg(target_os = "macos")] + fn write_to_find_pasteboard(&self, item: ClipboardItem) { + *self.current_find_pasteboard_item.lock() = Some(item); + } + + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Ok(())) + } + + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } + + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn register_url_scheme(&self, _: &str) -> Task> { + unimplemented!() + } + + fn open_with_system(&self, _path: &Path) { + unimplemented!() + } +} + +impl TestScreenCaptureSource { + /// Create a fake screen capture source, for testing. + #[cfg(any(test, feature = "test-support"))] + pub fn new() -> Self { + Self {} + } +} + +struct TestKeyboardLayout; + +impl PlatformKeyboardLayout for TestKeyboardLayout { + fn id(&self) -> &str { + "zed.keyboard.example" + } + + fn name(&self) -> &str { + "zed.keyboard.example" + } +} diff --git a/vendor/gpui-pre/src/platform/test/window.rs b/vendor/gpui-pre/src/platform/test/window.rs new file mode 100644 index 0000000..14fb889 --- /dev/null +++ b/vendor/gpui-pre/src/platform/test/window.rs @@ -0,0 +1,591 @@ +use crate::{ + AnyWindowHandle, Bounds, DevicePixels, DispatchEventResult, GpuSpecs, HeadlessAtlas, Pixels, + PlatformAtlas, PlatformDisplay, PlatformHeadlessRenderer, PlatformInput, PlatformInputHandler, + PlatformWindow, Point, PromptButton, RequestFrameOptions, Scene, Size, TestPlatform, + TextInputConfiguration, TextInputStateChange, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControlArea, WindowInsets, WindowParams, WindowVisibility, +}; +use gpui_util::ResultExt as _; +#[cfg(any(test, feature = "test-support"))] +use image::RgbaImage; +use parking_lot::Mutex; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use std::{ + cell::Cell, + path::PathBuf, + rc::{Rc, Weak}, + sync::{self, Arc}, +}; + +pub(crate) struct TestWindowState { + pub(crate) bounds: Bounds, + pub(crate) handle: AnyWindowHandle, + display: Rc, + pub(crate) title: Option, + pub(crate) edited: bool, + pub(crate) document_path: Option, + platform: Weak, + // TODO: Replace with `Rc` + sprite_atlas: Arc, + renderer: Option>, + pub(crate) should_close_handler: Option bool>>, + hit_test_window_control_callback: Option Option>>, + input_callback: Option DispatchEventResult>>, + active_status_change_callback: Option>, + visibility: WindowVisibility, + visibility_callback: Option>, + hover_status_change_callback: Option>, + resize_callback: Option, f32)>>, + visual_viewport: Option>, + visual_viewport_callback: Option>, + insets: WindowInsets, + insets_callback: Option>, + virtual_keyboard_requests: usize, + virtual_keyboard_dismissals: usize, + moved_callback: Option>, + appearance_change_callback: Option>, + request_frame_callback: Option>, + frame_wake_count: Rc>, + frame_scheduled: bool, + frame_callback_pending: bool, + input_handler: Option, + text_input_configurations: Vec, + text_input_state_changes: Vec, + is_fullscreen: bool, + scale_factor: f32, + appearance: WindowAppearance, + external_drag_files: Vec<(PathBuf, bool)>, + start_external_drag_result: bool, +} + +#[derive(Clone)] +pub struct TestWindow(pub(crate) Rc>); + +// Test windows are not backed by a real platform window, so there is no raw +// handle to report; `NotSupported` is `raw_window_handle`'s variant for exactly this. +impl HasWindowHandle for TestWindow { + fn window_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl HasDisplayHandle for TestWindow { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Err(raw_window_handle::HandleError::NotSupported) + } +} + +impl TestWindow { + pub(crate) fn new( + handle: AnyWindowHandle, + params: WindowParams, + platform: Weak, + display: Rc, + renderer: Option>, + ) -> Self { + let sprite_atlas: Arc = match &renderer { + Some(r) => r.sprite_atlas(), + None => Arc::new(HeadlessAtlas::default()), + }; + Self(Rc::new(Mutex::new(TestWindowState { + bounds: params.bounds, + display, + platform, + handle, + sprite_atlas, + renderer, + title: Default::default(), + edited: false, + document_path: None, + should_close_handler: None, + hit_test_window_control_callback: None, + input_callback: None, + active_status_change_callback: None, + visibility: WindowVisibility::Visible, + visibility_callback: None, + hover_status_change_callback: None, + resize_callback: None, + visual_viewport: None, + visual_viewport_callback: None, + insets: WindowInsets::default(), + insets_callback: None, + virtual_keyboard_requests: 0, + virtual_keyboard_dismissals: 0, + moved_callback: None, + appearance_change_callback: None, + request_frame_callback: None, + frame_wake_count: Rc::new(Cell::new(0)), + frame_scheduled: false, + frame_callback_pending: false, + input_handler: None, + text_input_configurations: Vec::new(), + text_input_state_changes: Vec::new(), + is_fullscreen: false, + // Preserve the test platform's historical 2x default. + scale_factor: 2.0, + appearance: WindowAppearance::Light, + external_drag_files: Vec::new(), + start_external_drag_result: false, + }))) + } + pub fn simulate_scheduled_frame(&self) -> bool { + let callback = { + let mut state = self.0.lock(); + if !std::mem::take(&mut state.frame_scheduled) { + return false; + } + state.frame_callback_pending = false; + state.request_frame_callback.take() + }; + let Some(mut callback) = callback else { + self.0.lock().frame_scheduled = true; + return false; + }; + + callback(RequestFrameOptions::default()); + self.0.lock().request_frame_callback = Some(callback); + true + } + + pub fn frame_scheduled(&self) -> bool { + self.0.lock().frame_scheduled + } + + pub fn simulate_visibility_change(&self, visibility: WindowVisibility) { + let callback = { + let mut state = self.0.lock(); + state.visibility = visibility; + state.visibility_callback.take() + }; + if let Some(mut callback) = callback { + callback(visibility); + self.0.lock().visibility_callback = Some(callback); + } + } + + pub fn simulate_visual_viewport_change(&self, bounds: Bounds) { + let callback = { + let mut state = self.0.lock(); + state.visual_viewport = Some(bounds); + state.visual_viewport_callback.take() + }; + if let Some(mut callback) = callback { + callback(); + self.0.lock().visual_viewport_callback = Some(callback); + } + } + + pub fn simulate_insets_change(&self, insets: WindowInsets) { + let callback = { + let mut state = self.0.lock(); + state.insets = insets.clone(); + state.insets_callback.take() + }; + if let Some(mut callback) = callback { + callback(insets); + self.0.lock().insets_callback = Some(callback); + } + } + + pub fn virtual_keyboard_requests(&self) -> usize { + self.0.lock().virtual_keyboard_requests + } + + pub fn virtual_keyboard_dismissals(&self) -> usize { + self.0.lock().virtual_keyboard_dismissals + } + + /// Every [`TextInputConfiguration`] forwarded to this window, in order. + pub fn text_input_configurations(&self) -> Vec { + self.0.lock().text_input_configurations.clone() + } + + pub fn text_input_state_changes(&self) -> Vec { + self.0.lock().text_input_state_changes.clone() + } + + pub fn simulate_resize(&mut self, size: Size) { + let scale_factor = self.scale_factor(); + let mut lock = self.0.lock(); + // Always update bounds, even if no callback is registered + lock.bounds.size = size; + let Some(mut callback) = lock.resize_callback.take() else { + return; + }; + drop(lock); + callback(size, scale_factor); + self.0.lock().resize_callback = Some(callback); + } + + /// Simulates a display scale change through the resize callback, preserving logical bounds. + pub fn simulate_scale_factor_change(&mut self, scale_factor: f32) { + let size = { + let mut lock = self.0.lock(); + lock.scale_factor = scale_factor; + lock.bounds.size + }; + self.simulate_resize(size); + } + + pub(crate) fn simulate_active_status_change(&self, active: bool) { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.active_status_change_callback.take() else { + return; + }; + drop(lock); + callback(active); + self.0.lock().active_status_change_callback = Some(callback); + } + + pub fn simulate_appearance_change(&self, appearance: WindowAppearance) { + let mut lock = self.0.lock(); + lock.appearance = appearance; + let Some(mut callback) = lock.appearance_change_callback.take() else { + return; + }; + drop(lock); + callback(); + self.0.lock().appearance_change_callback = Some(callback); + } + + /// Returns how many times this window's frame waker has been invoked. + pub fn frame_wake_count(&self) -> usize { + self.0.lock().frame_wake_count.get() + } + + /// Delivers a frame request to the window, as the platform's frame source + /// would. + pub fn simulate_frame_request(&self, options: RequestFrameOptions) { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.request_frame_callback.take() else { + return; + }; + drop(lock); + callback(options); + self.0.lock().request_frame_callback = Some(callback); + } + + pub fn simulate_input(&mut self, event: PlatformInput) -> bool { + let mut lock = self.0.lock(); + let Some(mut callback) = lock.input_callback.take() else { + return false; + }; + drop(lock); + let result = callback(event); + self.0.lock().input_callback = Some(callback); + !result.propagate + } + + pub fn external_drag_files(&self) -> Vec<(PathBuf, bool)> { + self.0.lock().external_drag_files.clone() + } + + pub fn set_start_external_drag_result(&self, result: bool) { + self.0.lock().start_external_drag_result = result; + } +} + +impl PlatformWindow for TestWindow { + fn visual_viewport_bounds(&self) -> Bounds { + let state = self.0.lock(); + state + .visual_viewport + .unwrap_or_else(|| Bounds::new(Point::default(), state.bounds.size)) + } + + fn on_visual_viewport_changed(&self, callback: Box) { + self.0.lock().visual_viewport_callback = Some(callback); + } + + fn insets(&self) -> WindowInsets { + self.0.lock().insets.clone() + } + + fn on_insets_changed(&self, callback: Box) { + self.0.lock().insets_callback = Some(callback); + } + + fn show_soft_keyboard(&self) { + self.0.lock().virtual_keyboard_requests += 1; + } + + fn hide_soft_keyboard(&self) { + self.0.lock().virtual_keyboard_dismissals += 1; + } + + fn bounds(&self) -> Bounds { + self.0.lock().bounds + } + + fn window_bounds(&self) -> WindowBounds { + WindowBounds::Windowed(self.bounds()) + } + + fn is_maximized(&self) -> bool { + false + } + + fn content_size(&self) -> Size { + self.bounds().size + } + + fn resize(&mut self, size: Size) { + let mut lock = self.0.lock(); + lock.bounds.size = size; + } + + fn scale_factor(&self) -> f32 { + self.0.lock().scale_factor + } + + fn appearance(&self) -> WindowAppearance { + self.0.lock().appearance + } + + fn display(&self) -> Option> { + Some(self.0.lock().display.clone()) + } + + fn mouse_position(&self) -> Point { + Point::default() + } + + fn modifiers(&self) -> crate::Modifiers { + crate::Modifiers::default() + } + + fn capslock(&self) -> crate::Capslock { + crate::Capslock::default() + } + + fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { + self.0.lock().input_handler = Some(input_handler); + } + + fn take_input_handler(&mut self) -> Option { + self.0.lock().input_handler.take() + } + + fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) { + self.0.lock().text_input_configurations.push(configuration); + } + + fn text_input_state_changed(&self, change: TextInputStateChange) { + self.0.lock().text_input_state_changes.push(change); + } + + fn prompt( + &self, + _level: crate::PromptLevel, + msg: &str, + detail: Option<&str>, + answers: &[PromptButton], + ) -> Option> { + Some( + self.0 + .lock() + .platform + .upgrade() + .expect("platform dropped") + .prompt(msg, detail, answers), + ) + } + + fn activate(&self) { + self.0 + .lock() + .platform + .upgrade() + .unwrap() + .set_active_window(Some(self.clone())) + } + + fn is_active(&self) -> bool { + false + } + + fn visibility(&self) -> WindowVisibility { + self.0.lock().visibility + } + + fn is_hovered(&self) -> bool { + false + } + + fn background_appearance(&self) -> WindowBackgroundAppearance { + WindowBackgroundAppearance::Opaque + } + + fn is_subpixel_rendering_supported(&self) -> bool { + false + } + + fn set_title(&mut self, title: &str) { + self.0.lock().title = Some(title.to_owned()); + } + + fn set_app_id(&mut self, _app_id: &str) {} + + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} + + fn set_edited(&mut self, edited: bool) { + self.0.lock().edited = edited; + } + + fn set_document_path(&self, path: Option<&std::path::Path>) { + self.0.lock().document_path = path.map(|p| p.to_path_buf()); + } + + fn show_character_palette(&self) { + unimplemented!() + } + + fn minimize(&self) { + unimplemented!() + } + + fn zoom(&self) { + unimplemented!() + } + + fn toggle_fullscreen(&self) { + let mut lock = self.0.lock(); + lock.is_fullscreen = !lock.is_fullscreen; + } + + fn is_fullscreen(&self) -> bool { + self.0.lock().is_fullscreen + } + + fn frame_waker(&self) -> Option> { + // Tests can inspect wakes without delivering a frame synchronously. + let frame_wake_count = self.0.lock().frame_wake_count.clone(); + #[cfg(feature = "bench-support")] + let window = Rc::downgrade(&self.0); + Some(Rc::new(move || { + frame_wake_count.set(frame_wake_count.get() + 1); + #[cfg(feature = "bench-support")] + if let Some(window) = window.upgrade() { + TestWindow(window).schedule_frame(); + } + })) + } + + fn on_request_frame(&self, callback: Box) { + self.0.lock().request_frame_callback = Some(callback); + } + + fn schedule_frame(&self) { + let mut state = self.0.lock(); + if !state.frame_callback_pending { + state.frame_scheduled = true; + } + } + + fn on_input(&self, callback: Box DispatchEventResult>) { + self.0.lock().input_callback = Some(callback) + } + + fn on_active_status_change(&self, callback: Box) { + self.0.lock().active_status_change_callback = Some(callback) + } + + fn on_visibility_change(&self, callback: Box) { + self.0.lock().visibility_callback = Some(callback); + } + + fn on_hover_status_change(&self, callback: Box) { + self.0.lock().hover_status_change_callback = Some(callback) + } + + fn on_resize(&self, callback: Box, f32)>) { + self.0.lock().resize_callback = Some(callback) + } + + fn on_moved(&self, callback: Box) { + self.0.lock().moved_callback = Some(callback) + } + + fn on_should_close(&self, callback: Box bool>) { + self.0.lock().should_close_handler = Some(callback); + } + + fn on_close(&self, _callback: Box) {} + + fn on_hit_test_window_control(&self, callback: Box Option>) { + self.0.lock().hit_test_window_control_callback = Some(callback); + } + + fn on_appearance_changed(&self, callback: Box) { + self.0.lock().appearance_change_callback = Some(callback); + } + + fn draw(&self, scene: &Scene) { + let scale_factor = self.scale_factor(); + let mut state = self.0.lock(); + state.frame_callback_pending = true; + state.frame_scheduled = true; + let device_size: Size = state.bounds.size.to_device_pixels(scale_factor); + if let Some(renderer) = &mut state.renderer { + renderer.render_scene(scene, device_size).warn_on_err(); + } + } + + fn sprite_atlas(&self) -> sync::Arc { + self.0.lock().sprite_atlas.clone() + } + + #[cfg(any(test, feature = "test-support"))] + fn render_to_image(&self, scene: &Scene) -> anyhow::Result { + let scale_factor = self.scale_factor(); + let mut state = self.0.lock(); + let size = state.bounds.size; + if let Some(renderer) = &mut state.renderer { + let device_size: Size = size.to_device_pixels(scale_factor); + renderer.render_scene_to_image(scene, device_size) + } else { + anyhow::bail!("render_to_image not available: no HeadlessRenderer configured") + } + } + + fn as_test(&mut self) -> Option<&mut TestWindow> { + Some(self) + } + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + unimplemented!() + } + + fn show_window_menu(&self, _position: Point) { + unimplemented!() + } + + fn start_window_move(&self) { + unimplemented!() + } + + fn can_start_external_drag(&self) -> bool { + true + } + + fn start_external_drag(&self, payload: &crate::ExternalDragPayload) -> bool { + let mut state = self.0.lock(); + match payload { + crate::ExternalDragPayload::Files(paths) => { + state.external_drag_files.extend_from_slice(paths.entries()); + } + } + state.start_external_drag_result + } + + fn update_ime_position(&self, _bounds: Bounds) {} + + fn gpu_specs(&self) -> Option { + None + } +} diff --git a/vendor/gpui-pre/src/platform/threaded_dispatcher.rs b/vendor/gpui-pre/src/platform/threaded_dispatcher.rs new file mode 100644 index 0000000..b1fb497 --- /dev/null +++ b/vendor/gpui-pre/src/platform/threaded_dispatcher.rs @@ -0,0 +1,744 @@ +use std::{ + collections::BinaryHeap, + sync::Arc, + thread, + time::{Duration, Instant}, +}; + +use parking_lot::{Condvar, Mutex}; + +use crate::{ + PlatformDispatcher, Priority, RunnableVariant, profiler, + queue::{PriorityQueueReceiver, PriorityQueueSender}, +}; + +const MIN_THREADS: usize = 2; + +/// A multithreaded [`PlatformDispatcher`] for tests and benchmarks. +/// +/// Background tasks run in parallel on a pool of worker threads and timers fire +/// in real time on a dedicated timer thread, mirroring the production +/// dispatchers (see `LinuxDispatcher`). Main-thread tasks are queued until the +/// creating thread drains them via [`Self::run_until_idle`], since there is no +/// platform run loop pumping them. +/// +/// Unlike [`TestDispatcher`](crate::TestDispatcher), which runs everything on a +/// single thread with a virtual clock, work dispatched through this dispatcher +/// executes with production concurrency. +pub struct ThreadedDispatcher { + background_sender: PriorityQueueSender, + main_sender: PriorityQueueSender, + main_receiver: Mutex>, + timers: Arc, + idle: Arc, + main_thread_id: thread::ThreadId, +} + +/// Tracks how many background and timer runnables are queued or running so +/// [`ThreadedDispatcher::run_until_idle`] knows when to stop waiting. +#[derive(Default)] +struct IdleTracker { + inflight: Mutex, + condvar: Condvar, +} + +impl IdleTracker { + fn increment(&self) { + *self.inflight.lock() += 1; + } + + fn decrement(&self) { + let mut inflight = self.inflight.lock(); + *inflight -= 1; + if *inflight == 0 { + self.condvar.notify_all(); + } + } + + /// Returns a guard that decrements the in-flight count when dropped, so + /// the count stays correct even if the runnable being executed panics. + fn decrement_on_drop(&self) -> impl Drop + '_ { + gpui_util::defer(|| self.decrement()) + } + + /// Notifies waiters while holding the in-flight lock. `run_until_idle` + /// re-checks its wake conditions under this lock before waiting, so the + /// notification can't slip between its check and its wait and be lost. + fn notify_under_lock(&self) { + let _inflight = self.inflight.lock(); + self.condvar.notify_all(); + } +} + +struct TimerQueue { + state: Mutex, + condvar: Condvar, +} + +struct TimerQueueState { + heap: BinaryHeap, + next_seq: u64, +} + +struct TimerEntry { + due: Instant, + seq: u64, + runnable: RunnableVariant, +} + +impl PartialEq for TimerEntry { + fn eq(&self, other: &Self) -> bool { + self.due == other.due && self.seq == other.seq + } +} + +impl Eq for TimerEntry {} + +impl PartialOrd for TimerEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TimerEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Reversed so that the entry with the earliest due time (breaking ties + // by insertion order) is at the top of the max-heap. + other + .due + .cmp(&self.due) + .then_with(|| other.seq.cmp(&self.seq)) + } +} + +impl Default for ThreadedDispatcher { + fn default() -> Self { + Self::new() + } +} + +impl ThreadedDispatcher { + /// Creates a dispatcher whose main thread is the calling thread. + /// + /// Worker and timer threads live for the lifetime of the process; the + /// dispatcher is expected to be created once and reused. + pub fn new() -> Self { + let (background_sender, background_receiver) = PriorityQueueReceiver::new(); + let (main_sender, main_receiver) = PriorityQueueReceiver::new(); + let idle = Arc::new(IdleTracker::default()); + + let thread_count = + thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS)); + for i in 0..thread_count { + let mut receiver: PriorityQueueReceiver = background_receiver.clone(); + let idle = idle.clone(); + thread::Builder::new() + .name(format!("ThreadedDispatcherWorker-{i}")) + .spawn(move || { + while let Ok(runnable) = receiver.pop() { + let _decrement = idle.decrement_on_drop(); + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + } + }) + .expect("failed to spawn threaded dispatcher worker"); + } + drop(background_receiver); + + let timers = Arc::new(TimerQueue { + state: Mutex::new(TimerQueueState { + heap: BinaryHeap::new(), + next_seq: 0, + }), + condvar: Condvar::new(), + }); + { + let timers = timers.clone(); + let idle = idle.clone(); + thread::Builder::new() + .name("ThreadedDispatcherTimer".to_owned()) + .spawn(move || { + let mut state = timers.state.lock(); + loop { + let Some(entry) = state.heap.peek() else { + timers.condvar.wait(&mut state); + continue; + }; + let due = entry.due; + if due > Instant::now() { + timers.condvar.wait_until(&mut state, due); + continue; + } + let Some(entry) = state.heap.pop() else { + continue; + }; + // Count the firing timer as in-flight before releasing + // the lock so it can spawn follow-up work that + // `run_until_idle` will wait for. Lock order is always + // timer state, then in-flight count; `run_until_idle` + // never takes them in the opposite order. + idle.increment(); + drop(state); + + { + let _decrement = idle.decrement_on_drop(); + let location = entry.runnable.metadata().location; + let spawned = entry.runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + entry.runnable.run(); + profiler::save_task_timing(); + } + + state = timers.state.lock(); + } + }) + .expect("failed to spawn threaded dispatcher timer"); + } + + Self { + background_sender, + main_sender, + main_receiver: Mutex::new(main_receiver), + timers, + idle, + main_thread_id: thread::current().id(), + } + } + + /// Runs queued main thread tasks and waits until no background or timer + /// work is queued, running, or already due. + /// + /// Timers that haven't reached their due time yet are *not* waited for: + /// the dispatcher runs in real time and cannot skip ahead like the + /// `TestDispatcher`'s virtual clock, so waiting on a future timer would + /// block for its full real duration. Tasks sleeping on such timers are + /// considered idle. Must be called on the thread that created this + /// dispatcher. + pub fn run_until_idle(&self) { + self.run_until_idle_with(|| self.drain_main_queue()); + } + + pub(crate) fn run_until_idle_with(&self, mut run_turn: impl FnMut() -> bool) { + assert!( + self.is_main_thread(), + "run_until_idle must be called on the threaded dispatcher's main thread" + ); + loop { + if run_turn() { + continue; + } + + // Checked before taking the in-flight lock; the timer thread + // locks them in the opposite order, so nesting would deadlock. + if self.has_due_timer() { + // Poll briefly: a firing timer leaves the heap just before it + // registers as in-flight. + let mut inflight = self.idle.inflight.lock(); + self.idle + .condvar + .wait_for(&mut inflight, Duration::from_millis(1)); + continue; + } + + let mut inflight = self.idle.inflight.lock(); + // Re-checked under the lock that `dispatch_on_main_thread` + // notifies under, so the notification can't be lost. + if self.main_queue_has_work() { + continue; + } + if *inflight == 0 { + // Main-thread sends happen before in-flight decrements, and + // decrements happen under this lock, so the check above + // observed all completed work. + return; + } + // Woken when main-thread work arrives or the in-flight count + // reaches zero; both notify under this lock. + self.idle.condvar.wait(&mut inflight); + } + } + + /// Drives main-thread work until `ready` returns a value. + /// + /// Unlike [`Self::run_until_idle`], this waits across temporary quiescence. + /// This is required when completion can arrive from an external worker that + /// is not represented in the dispatcher's in-flight count. + /// + /// Readiness is checked before every main-thread runnable, so this returns + /// as soon as `ready` observes completion rather than after the queue + /// drains — deferred work that re-queues itself (idle sweeps, pollers) + /// must not extend a benchmark's measured interval past the completion it + /// awaits. + #[cfg(test)] + pub(crate) fn run_until(&self, ready: impl FnMut() -> Option) -> R { + self.run_until_with_frames(ready, || false) + } + + /// Runs at most one queued main-thread task, returning whether one ran. + /// + /// Stepping tasks individually lets callers check completion between polls + /// even when work continually re-queues itself. + fn run_one_main_task(&self) -> bool { + assert!( + self.is_main_thread(), + "main tasks must run on the main thread" + ); + let runnable = self.main_receiver.lock().try_pop(); + match runnable { + Ok(Some(runnable)) => { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + true + } + Ok(None) | Err(_) => false, + } + } + + /// Runs up to the initial queue length of main-thread tasks. + /// + /// Returns whether any ran. This snapshots the count, not task identities: + /// newly dispatched higher-priority tasks can overtake the original tasks. + pub fn run_ready_main_tasks(&self) -> bool { + self.run_ready_main_tasks_with(|| true, || {}) + } + + /// Runs a count-bounded ready turn, checking `should_continue` before every poll. + /// Calls `after_poll` after each runnable returns, outside the queue lock. + pub(crate) fn run_ready_main_tasks_with( + &self, + mut should_continue: impl FnMut() -> bool, + mut after_poll: impl FnMut(), + ) -> bool { + assert!( + self.is_main_thread(), + "run_ready_main_tasks must be called on the threaded dispatcher's main thread" + ); + let pending = self.main_receiver.lock().len(); + let mut ran_any = false; + for _ in 0..pending { + if !should_continue() || !self.run_one_main_task() { + break; + } + ran_any = true; + after_poll(); + } + ran_any + } + + #[cfg(any(test, feature = "bench-support"))] + pub(crate) fn run_until_with_frames( + &self, + mut ready: impl FnMut() -> Option, + mut dispatch_frames: impl FnMut() -> bool, + ) -> R { + assert!( + self.is_main_thread(), + "main tasks must run on the main thread" + ); + loop { + if let Some(result) = ready() { + return result; + } + let ran_task = self.run_one_main_task(); + // A completion poll must not pull an unrelated animation frame + // into the measured operation. + if let Some(result) = ready() { + return result; + } + let dispatched_frames = dispatch_frames(); + if ran_task || dispatched_frames { + continue; + } + + let mut inflight = self.idle.inflight.lock(); + if self.main_queue_has_work() { + continue; + } + self.idle.condvar.wait(&mut inflight); + } + } + + /// Cancels all pending timers so timers armed by one workload can't fire + /// during a later workload sharing this process-lifetime dispatcher. + /// + /// Dropping a timer runnable drops its completion sender, waking the task + /// awaiting the timer. Call [`Self::run_until_idle`] after this method to + /// drain any work that cancellation unblocks. + pub fn cancel_pending_timers(&self) -> usize { + let timers = { + let mut state = self.timers.state.lock(); + let timers: Vec<_> = state.heap.drain().collect(); + self.timers.condvar.notify_all(); + timers + }; + let canceled = timers.len(); + drop(timers); + canceled + } + + /// Describes the dispatcher's idle-tracking state, for diagnosing + /// workloads that fail to reach quiescence. + pub fn debug_state(&self) -> String { + let inflight = *self.idle.inflight.lock(); + let timers = self.timers.state.lock().heap.len(); + let main_queue_has_work = self.main_queue_has_work(); + format!( + "ThreadedDispatcher {{ inflight: {inflight}, pending_timers: {timers}, \ + main_queue_has_work: {main_queue_has_work} }}" + ) + } + + /// Whether no main-thread work is queued, no background or timer + /// runnables are queued or running, and no armed timer is due. Timers + /// that aren't due yet are ignored, as in [`Self::run_until_idle`]. + #[cfg(any(test, feature = "bench-support"))] + pub(crate) fn is_idle(&self) -> bool { + !self.main_queue_has_work() && !self.has_due_timer() && *self.idle.inflight.lock() == 0 + } + + fn has_due_timer(&self) -> bool { + let state = self.timers.state.lock(); + state + .heap + .peek() + .is_some_and(|entry| entry.due <= Instant::now()) + } + + fn main_queue_has_work(&self) -> bool { + !self.main_receiver.lock().is_empty() + } + + fn drain_main_queue(&self) -> bool { + let mut ran_any = false; + loop { + // Lock only around the pop so runnables can re-entrantly dispatch + // more main-thread work through the sender while they run. + let runnable = self.main_receiver.lock().try_pop(); + match runnable { + Ok(Some(runnable)) => { + let location = runnable.metadata().location; + let spawned = runnable.metadata().spawned; + profiler::update_running_task(spawned, location); + runnable.run(); + profiler::save_task_timing(); + ran_any = true; + } + Ok(None) | Err(_) => return ran_any, + } + } + } +} + +impl PlatformDispatcher for ThreadedDispatcher { + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } + + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + self.idle.increment(); + self.background_sender + .send(priority, runnable) + .unwrap_or_else(|_| panic!("threaded dispatcher workers are no longer running")); + } + + fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { + if let Err(error) = self.main_sender.send(priority, runnable) { + // The main receiver lives as long as this dispatcher, so a failed + // send means we're mid-teardown. The runnable may wrap a !Send + // future, so forget it rather than dropping it on this thread + // (mirrors LinuxDispatcher). + std::mem::forget(error); + return; + } + // Wake `run_until_idle` if it's waiting for main-thread work. + self.idle.notify_under_lock(); + } + + fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) { + let mut state = self.timers.state.lock(); + let seq = state.next_seq; + state.next_seq += 1; + state.heap.push(TimerEntry { + due: Instant::now() + duration, + seq, + runnable, + }); + self.timers.condvar.notify_one(); + } + + fn spawn_realtime(&self, f: Box) { + // This dispatcher does not need realtime scheduling priority; a plain + // thread keeps it portable. + thread::Builder::new() + .name("ThreadedDispatcherRealtime".to_owned()) + .spawn(f) + .expect("failed to spawn threaded dispatcher realtime thread"); + } + + fn as_threaded(&self) -> Option<&ThreadedDispatcher> { + Some(self) + } +} + +#[cfg(test)] +mod tests { + use std::future::Future; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use super::*; + use crate::{BackgroundExecutor, ForegroundExecutor}; + + #[test] + fn is_idle_tracks_queued_work_but_ignores_undue_timers() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + assert!(dispatcher.is_idle()); + + foreground.spawn(async {}).detach(); + assert!(!dispatcher.is_idle()); + dispatcher.run_until_idle(); + assert!(dispatcher.is_idle()); + + let background = BackgroundExecutor::new(dispatcher.clone()); + let timer = background.timer(Duration::from_secs(60)); + // The timer future's initial poll runs on a worker thread; wait for + // it so only the armed, not-yet-due timer remains. + dispatcher.run_until_idle(); + assert!( + dispatcher.is_idle(), + "a timer that is not due yet should not count as pending work" + ); + drop(timer); + dispatcher.cancel_pending_timers(); + dispatcher.run_until_idle(); + assert!(dispatcher.is_idle()); + } + + #[test] + fn run_ready_main_tasks_does_not_wait_for_background_handoffs() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let background = BackgroundExecutor::new(dispatcher.clone()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + + let (sender, receiver) = futures::channel::oneshot::channel(); + background + .spawn(async move { + thread::sleep(Duration::from_millis(10)); + sender.send(()).ok(); + }) + .detach(); + + let completed = Arc::new(AtomicBool::new(false)); + foreground + .spawn({ + let completed = completed.clone(); + async move { + receiver.await.ok(); + completed.store(true, Ordering::SeqCst); + } + }) + .detach(); + + assert!(dispatcher.run_ready_main_tasks()); + assert!(!completed.load(Ordering::SeqCst)); + + dispatcher.run_until_idle(); + assert!(completed.load(Ordering::SeqCst)); + } + + #[test] + fn run_until_idle_completes_background_to_main_handoffs() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let background = BackgroundExecutor::new(dispatcher.clone()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + + let (sender, receiver) = futures::channel::oneshot::channel(); + background + .spawn(async move { + thread::sleep(Duration::from_millis(10)); + sender.send(()).ok(); + }) + .detach(); + + let completed = Arc::new(AtomicBool::new(false)); + foreground + .spawn({ + let completed = completed.clone(); + async move { + receiver.await.ok(); + completed.store(true, Ordering::SeqCst); + } + }) + .detach(); + + dispatcher.run_until_idle(); + assert!(completed.load(Ordering::SeqCst)); + } + + #[test] + fn run_until_waits_for_untracked_external_wakes() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + let (sender, receiver) = futures::channel::oneshot::channel(); + let sender_thread = thread::spawn(move || { + thread::sleep(Duration::from_millis(10)); + sender + .send(()) + .expect("foreground receiver should remain alive"); + }); + + let completed = Arc::new(AtomicBool::new(false)); + foreground + .spawn({ + let completed = completed.clone(); + async move { + receiver + .await + .expect("external sender should deliver its wake"); + completed.store(true, Ordering::SeqCst); + } + }) + .detach(); + + dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(())); + sender_thread.join().expect("sender thread should finish"); + assert!(completed.load(Ordering::SeqCst)); + } + + #[test] + fn run_until_returns_at_readiness_despite_requeuing_main_work() { + const REQUEUE_LIMIT: usize = 10_000; + + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + + // Mirrors main-thread work that yields and immediately re-queues + // itself (e.g. an idle-time sweep): the main queue never drains until + // such work finishes every iteration, so readiness must be observed + // between runnables rather than only at quiescence. + let iterations = Arc::new(AtomicUsize::new(0)); + foreground + .spawn({ + let iterations = iterations.clone(); + async move { + for _ in 0..REQUEUE_LIMIT { + iterations.fetch_add(1, Ordering::SeqCst); + yield_once().await; + } + } + }) + .detach(); + + let completed = Arc::new(AtomicBool::new(false)); + foreground + .spawn({ + let completed = completed.clone(); + async move { + completed.store(true, Ordering::SeqCst); + } + }) + .detach(); + + dispatcher.run_until(|| completed.load(Ordering::SeqCst).then_some(())); + assert!( + iterations.load(Ordering::SeqCst) < REQUEUE_LIMIT, + "run_until should return at readiness instead of draining re-queued main work" + ); + } + + /// Completes after one re-schedule: the poll returns `Pending` and wakes + /// immediately, so the runnable re-enters the main queue. + fn yield_once() -> impl Future { + let mut yielded = false; + std::future::poll_fn(move |poll_context| { + if yielded { + std::task::Poll::Ready(()) + } else { + yielded = true; + poll_context.waker().wake_by_ref(); + std::task::Poll::Pending + } + }) + } + + #[test] + fn run_ready_main_tasks_advances_requeuing_work_one_batch_per_call() { + const REQUEUE_LIMIT: usize = 10_000; + + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + + let iterations = Arc::new(AtomicUsize::new(0)); + foreground + .spawn({ + let iterations = iterations.clone(); + async move { + for _ in 0..REQUEUE_LIMIT { + iterations.fetch_add(1, Ordering::SeqCst); + yield_once().await; + } + } + }) + .detach(); + + assert!(dispatcher.run_ready_main_tasks()); + assert_eq!(iterations.load(Ordering::SeqCst), 1); + assert!(dispatcher.run_ready_main_tasks()); + assert_eq!(iterations.load(Ordering::SeqCst), 2); + } + + #[test] + fn timers_fire_in_real_time() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let background = BackgroundExecutor::new(dispatcher); + + let fired = Arc::new(AtomicBool::new(false)); + let timer = background.timer(Duration::from_millis(10)); + background + .spawn({ + let fired = fired.clone(); + async move { + timer.await; + fired.store(true, Ordering::SeqCst); + } + }) + .detach(); + + let deadline = Instant::now() + Duration::from_secs(10); + while !fired.load(Ordering::SeqCst) && Instant::now() < deadline { + thread::sleep(Duration::from_millis(1)); + } + assert!(fired.load(Ordering::SeqCst)); + } + + #[test] + fn cancel_pending_timers_wakes_waiters_without_waiting_for_deadline() { + let dispatcher = Arc::new(ThreadedDispatcher::new()); + let background = BackgroundExecutor::new(dispatcher.clone()); + + let fired = Arc::new(AtomicBool::new(false)); + let timer = background.timer(Duration::from_secs(10)); + background + .spawn({ + let fired = fired.clone(); + async move { + timer.await; + fired.store(true, Ordering::SeqCst); + } + }) + .detach(); + + dispatcher.run_until_idle(); + assert_eq!(dispatcher.cancel_pending_timers(), 1); + dispatcher.run_until_idle(); + + assert!(fired.load(Ordering::SeqCst)); + assert_eq!(dispatcher.cancel_pending_timers(), 0); + } +} diff --git a/vendor/gpui-pre/src/platform/visual_test.rs b/vendor/gpui-pre/src/platform/visual_test.rs new file mode 100644 index 0000000..0454c31 --- /dev/null +++ b/vendor/gpui-pre/src/platform/visual_test.rs @@ -0,0 +1,272 @@ +//! Visual test platform that combines real rendering (macOs-only for now) with controllable TestDispatcher. +//! +//! This platform is used for visual tests that need: +//! - Real rendering (e.g. Metal/compositor) for accurate screenshots +//! - Deterministic task scheduling via TestDispatcher +//! - Controllable time via `advance_clock` + +use crate::ScreenCaptureSource; +use crate::{ + ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, + ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, + PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, + PlatformWindow, Task, TestDispatcher, WindowAppearance, WindowParams, +}; +use anyhow::Result; +use futures::channel::oneshot; +use parking_lot::Mutex; + +use std::{ + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, +}; + +/// A platform that combines real Mac rendering with controllable TestDispatcher. +/// +/// This allows visual tests to: +/// - Render real UI via Metal for accurate screenshots +/// - Control task scheduling deterministically via TestDispatcher +/// - Advance simulated time for testing time-based behaviors (tooltips, animations, etc.) +pub struct VisualTestPlatform { + dispatcher: TestDispatcher, + background_executor: BackgroundExecutor, + foreground_executor: ForegroundExecutor, + platform: Rc, + clipboard: Mutex>, + find_pasteboard: Mutex>, +} + +impl VisualTestPlatform { + /// Creates a new VisualTestPlatform with the given random seed. + /// + /// The seed is used for deterministic random number generation in the TestDispatcher. + pub fn new(platform: Rc, seed: u64) -> Self { + let dispatcher = TestDispatcher::new(seed); + let arc_dispatcher = Arc::new(dispatcher.clone()); + + let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); + let foreground_executor = ForegroundExecutor::new(arc_dispatcher); + + Self { + dispatcher, + background_executor, + foreground_executor, + platform, + clipboard: Mutex::new(None), + find_pasteboard: Mutex::new(None), + } + } + + /// Returns a reference to the TestDispatcher for controlling task scheduling and time. + pub fn dispatcher(&self) -> &TestDispatcher { + &self.dispatcher + } +} + +impl Platform for VisualTestPlatform { + fn background_executor(&self) -> BackgroundExecutor { + self.background_executor.clone() + } + + fn foreground_executor(&self) -> ForegroundExecutor { + self.foreground_executor.clone() + } + + fn text_system(&self) -> Arc { + self.platform.text_system() + } + + fn run(&self, _on_finish_launching: Box) { + panic!("VisualTestPlatform::run should not be called in tests") + } + + fn quit(&self) {} + + fn restart(&self, _binary_path: Option, _arguments: Vec) {} + + fn activate(&self, _ignoring_other_apps: bool) {} + + fn hide(&self) {} + + fn hide_other_apps(&self) {} + + fn unhide_other_apps(&self) {} + + fn displays(&self) -> Vec> { + self.platform.displays() + } + + fn primary_display(&self) -> Option> { + self.platform.primary_display() + } + + fn active_window(&self) -> Option { + self.platform.active_window() + } + + fn window_stack(&self) -> Option> { + self.platform.window_stack() + } + + fn is_screen_capture_supported(&self) -> bool { + self.platform.is_screen_capture_supported() + } + + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.platform.screen_capture_sources() + } + + fn open_window( + &self, + handle: AnyWindowHandle, + options: WindowParams, + ) -> Result> { + self.platform.open_window(handle, options) + } + + fn window_appearance(&self) -> WindowAppearance { + self.platform.window_appearance() + } + + fn open_url(&self, url: &str) { + self.platform.open_url(url) + } + + fn on_open_urls(&self, _callback: Box)>) {} + + fn register_url_scheme(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn prompt_for_paths( + &self, + _options: PathPromptOptions, + ) -> oneshot::Receiver>>> { + let (tx, rx) = oneshot::channel(); + tx.send(Ok(None)).ok(); + rx + } + + fn prompt_for_new_path( + &self, + _directory: &Path, + _suggested_name: Option<&str>, + ) -> oneshot::Receiver>> { + let (tx, rx) = oneshot::channel(); + tx.send(Ok(None)).ok(); + rx + } + + fn can_select_mixed_files_and_dirs(&self) -> bool { + true + } + + fn reveal_path(&self, path: &Path) { + self.platform.reveal_path(path) + } + + fn open_with_system(&self, path: &Path) { + self.platform.open_with_system(path) + } + + fn on_quit(&self, _callback: Box bool>) {} + + fn on_reopen(&self, _callback: Box) {} + + fn on_system_sleep(&self, _callback: Box) {} + + fn on_system_wake(&self, _callback: Box) {} + + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} + + fn get_menus(&self) -> Option> { + None + } + + fn set_dock_menu(&self, _menu: Vec, _keymap: &Keymap) {} + + fn on_app_menu_action(&self, _callback: Box) {} + + fn on_will_open_app_menu(&self, _callback: Box) {} + + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} + + fn app_path(&self) -> Result { + self.platform.app_path() + } + + fn path_for_auxiliary_executable(&self, name: &str) -> Result { + self.platform.path_for_auxiliary_executable(name) + } + + fn set_cursor_style(&self, style: CursorStyle) { + self.platform.set_cursor_style(style) + } + + fn hide_cursor_until_mouse_moves(&self) { + self.platform.hide_cursor_until_mouse_moves(); + } + + fn is_cursor_visible(&self) -> bool { + self.platform.is_cursor_visible() + } + + fn should_auto_hide_scrollbars(&self) -> bool { + self.platform.should_auto_hide_scrollbars() + } + + fn read_from_clipboard(&self) -> Option { + self.clipboard.lock().clone() + } + + fn write_to_clipboard(&self, item: ClipboardItem) { + *self.clipboard.lock() = Some(item); + } + + #[cfg(target_os = "macos")] + fn read_from_find_pasteboard(&self) -> Option { + self.find_pasteboard.lock().clone() + } + + #[cfg(target_os = "macos")] + fn write_to_find_pasteboard(&self, item: ClipboardItem) { + *self.find_pasteboard.lock() = Some(item); + } + + fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task> { + Task::ready(Ok(())) + } + + fn read_credentials(&self, _url: &str) -> Task)>>> { + Task::ready(Ok(None)) + } + + fn delete_credentials(&self, _url: &str) -> Task> { + Task::ready(Ok(())) + } + + fn keyboard_layout(&self) -> Box { + self.platform.keyboard_layout() + } + + fn keyboard_mapper(&self) -> Rc { + self.platform.keyboard_mapper() + } + + fn on_keyboard_layout_change(&self, _callback: Box) {} + + fn thermal_state(&self) -> super::ThermalState { + super::ThermalState::Nominal + } + + fn on_thermal_state_change(&self, _callback: Box) {} + + fn prevent_idle_sleep(&self, reason: &str) -> Task> { + Task::ready(Err(anyhow::anyhow!( + "Idle sleep prevention for {reason:?} is not supported in visual tests" + ))) + } +} diff --git a/vendor/gpui-pre/src/platform_scheduler.rs b/vendor/gpui-pre/src/platform_scheduler.rs new file mode 100644 index 0000000..a555a5a --- /dev/null +++ b/vendor/gpui-pre/src/platform_scheduler.rs @@ -0,0 +1,457 @@ +use crate::{PlatformDispatcher, RunnableMeta}; +use async_task::Runnable; +use chrono::{DateTime, Utc}; +use futures::channel::oneshot; +use scheduler::Instant; +use scheduler::{ + Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer, + spawn_dedicated_thread, +}; +#[cfg(not(target_family = "wasm"))] +use std::task::{Context, Poll}; +use std::{ + any::Any, + future::Future, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicU16, Ordering}, + }, + time::Duration, +}; + +/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`]. +/// +/// This allows GPUI to use the scheduler crate's executor types with the platform's +/// native dispatch mechanisms (e.g., Grand Central Dispatch on macOS). +pub struct PlatformScheduler { + dispatcher: Arc, + clock: Arc, + next_session_id: AtomicU16, + #[cfg(feature = "profiler")] + foreground_runnables: crate::profiler::journal::ForegroundRunnableCounter, +} + +impl PlatformScheduler { + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher: dispatcher.clone(), + clock: Arc::new(PlatformClock { dispatcher }), + next_session_id: AtomicU16::new(0), + #[cfg(feature = "profiler")] + foreground_runnables: crate::profiler::journal::foreground_runnable_counter(), + } + } + + pub fn foreground_executor(self: &Arc) -> LocalExecutor { + let session_id = self.next_session_id(); + let scheduler = Arc::downgrade(self); + LocalExecutor::new(session_id, self.clone(), move |runnable| { + if let Some(scheduler) = scheduler.upgrade() { + scheduler.schedule_local(session_id, runnable); + } + }) + } + + fn next_session_id(&self) -> SessionId { + SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst)) + } + + #[cfg(feature = "profiler")] + pub(crate) fn foreground_runnable_counter( + &self, + ) -> crate::profiler::journal::ForegroundRunnableCounter { + self.foreground_runnables.clone() + } +} + +impl Scheduler for PlatformScheduler { + #[cfg(not(target_family = "wasm"))] + fn block( + &self, + _session_id: Option, + mut future: Pin<&mut dyn Future>, + timeout: Option, + ) -> bool { + use waker_fn::waker_fn; + let deadline = timeout.map(|t| Instant::now() + t); + let parker = parking::Parker::new(); + let unparker = parker.unparker(); + let waker = waker_fn(move || { + unparker.unpark(); + }); + let mut cx = Context::from_waker(&waker); + if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { + return true; + } + + let park_deadline = |deadline: Instant| { + // Timer expirations are only delivered every ~15.6 milliseconds by default on Windows. + // We increase the resolution during this wait so that short timeouts stay reasonably short. + let _timer_guard = self.dispatcher.increase_timer_resolution(); + parker.park_deadline(deadline) + }; + + loop { + match deadline { + Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => { + return false; + } + Some(_) => (), + None => parker.park(), + } + if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { + break true; + } + } + } + + fn schedule_local(&self, _session_id: SessionId, runnable: Runnable) { + #[cfg(feature = "profiler")] + self.foreground_runnables.queued(); + self.dispatcher + .dispatch_on_main_thread(runnable, Priority::default()); + } + + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ) { + self.dispatcher.dispatch(runnable, priority); + } + + fn spawn_realtime(&self, f: Box) { + self.dispatcher.spawn_realtime(f); + } + + #[track_caller] + fn timer(&self, duration: Duration) -> Timer { + let (tx, rx) = oneshot::channel(); + let dispatcher = self.dispatcher.clone(); + + // Create a runnable that will send the completion signal + let location = std::panic::Location::caller(); + let (runnable, _task) = async_task::Builder::new() + .metadata(RunnableMeta { + location, + spawned: scheduler::SpawnTime(Instant::now()), + }) + .spawn( + move |_| async move { + let _ = tx.send(()); + }, + move |runnable| { + dispatcher.dispatch_after(duration, runnable); + }, + ); + runnable.schedule(); + + Timer::new(rx) + } + + fn clock(&self) -> Arc { + self.clock.clone() + } + + fn spawn_dedicated( + self: Arc, + f: Box< + dyn FnOnce( + LocalExecutor, + ) + -> Pin> + 'static>> + + Send + + 'static, + >, + ) -> Task> { + let session_id = self.next_session_id(); + spawn_dedicated_thread(session_id, self, move |executor| f(executor)) + } + + fn as_test(&self) -> Option<&TestScheduler> { + None + } +} + +/// A production clock that uses the platform dispatcher's time. +struct PlatformClock { + dispatcher: Arc, +} + +impl Clock for PlatformClock { + fn utc_now(&self) -> DateTime { + Utc::now() + } + + fn now(&self) -> Instant { + self.dispatcher.now() + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + use crate::RunnableVariant; + use scheduler::BackgroundExecutor; + use std::time::Instant as StdInstant; + + // `spawn_dedicated` shouldn't touch the platform dispatcher at all; + // panicking on every method ensures the test catches it if it does. + struct SmokeDispatcher; + + impl PlatformDispatcher for SmokeDispatcher { + fn is_main_thread(&self) -> bool { + false + } + fn dispatch(&self, _runnable: RunnableVariant, _priority: Priority) { + panic!("SmokeDispatcher should not be asked to dispatch in this test"); + } + fn dispatch_on_main_thread(&self, _runnable: RunnableVariant, _priority: Priority) { + panic!("SmokeDispatcher does not implement a main thread"); + } + fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) { + panic!("SmokeDispatcher does not implement timers"); + } + fn spawn_realtime(&self, _f: Box) { + panic!("SmokeDispatcher does not implement realtime"); + } + } + + #[test] + fn dedicated_executor_tasks_share_one_thread() { + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + let dedicated = scheduler::DedicatedExecutor::new(&background); + + let first = dedicated.spawn(async { std::thread::current().id() }); + let second = dedicated.spawn(async { std::thread::current().id() }); + + let first = futures::executor::block_on(first); + let second = futures::executor::block_on(second); + + assert_eq!(first, second, "tasks must share the dedicated thread"); + assert_ne!( + first, + std::thread::current().id(), + "dedicated tasks must not run on the spawning thread" + ); + } + + #[test] + fn spawn_dedicated_runs_on_a_real_separate_thread() { + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + let started = StdInstant::now(); + let task = background.spawn_dedicated(|_executor| async move { + // A genuine blocking syscall on the dedicated thread. If + // `spawn_dedicated` were running the future on any shared + // executor, this would stall that executor. + let thread_id_before = std::thread::current().id(); + std::thread::sleep(Duration::from_millis(50)); + let thread_id_after = std::thread::current().id(); + assert_eq!(thread_id_before, thread_id_after); + (thread_id_before, "slept") + }); + let (dedicated_thread_id, message) = futures::executor::block_on(task); + let elapsed = started.elapsed(); + assert_eq!(message, "slept"); + assert_ne!( + dedicated_thread_id, + std::thread::current().id(), + "dedicated future ran on the test thread" + ); + assert!( + elapsed >= Duration::from_millis(40), + "expected the dedicated thread to genuinely sleep, elapsed = {:?}", + elapsed + ); + } + + #[test] + fn spawn_dedicated_returns_not_send_future_output() { + // The whole point of `spawn_dedicated` is that the future can be + // `!Send`. Constructing one with `Rc>` ensures the + // signature actually permits it. + use std::cell::RefCell; + use std::rc::Rc; + + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + let task = background.spawn_dedicated(|_executor| async move { + let state = Rc::new(RefCell::new(0_i32)); + for _ in 0..3 { + *state.borrow_mut() += 1; + } + *state.borrow() + }); + let output = futures::executor::block_on(task); + assert_eq!(output, 3); + } + + #[test] + fn spawn_dedicated_dropping_task_cancels_future() { + use parking_lot::Mutex; + use std::sync::mpsc; + + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + + let (started_tx, started_rx) = mpsc::channel::<()>(); + let (after_park_tx, after_park_rx) = mpsc::channel::<()>(); + let observed_post_await_write = Arc::new(Mutex::new(false)); + + let task = { + let observed_post_await_write = observed_post_await_write.clone(); + background.spawn_dedicated(move |_executor| async move { + // Announce that the future is live on the dedicated thread. + started_tx + .send(()) + .expect("started signal must be received"); + // Park forever. Dropping the `Task` must cancel us here so + // the code below this `await` never runs. + futures::future::pending::<()>().await; + *observed_post_await_write.lock() = true; + after_park_tx + .send(()) + .expect("after-park signal must be received"); + }) + }; + + // Wait until the dedicated future is actually parked at the await. + started_rx + .recv_timeout(Duration::from_secs(2)) + .expect("dedicated future failed to start"); + + // Drop the root Task: this must cancel the future. + drop(task); + + // If cancellation works, the future never advances past `pending`, + // so this recv must time out. + assert!( + after_park_rx + .recv_timeout(Duration::from_millis(100)) + .is_err(), + "dedicated future advanced past the await after its Task was dropped" + ); + assert!( + !*observed_post_await_write.lock(), + "dedicated future ran code past the cancellation point" + ); + } + + #[test] + fn spawn_dedicated_thread_tears_down_after_work_completes() { + use std::sync::mpsc; + + // Fires from `Drop` so we observe teardown of the dedicated future's + // captured state on whichever thread runs its destructor. + struct DropSignal { + tx: Option>, + } + impl Drop for DropSignal { + fn drop(&mut self) { + if let Some(tx) = self.tx.take() { + let _ = tx.send(std::thread::current().id()); + } + } + } + + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + let (started_tx, started_rx) = mpsc::channel::(); + let (drop_tx, drop_rx) = mpsc::channel::(); + + let task = background.spawn_dedicated(move |_executor| async move { + // Captured by the future's state. When the future completes and + // its state is dropped on the dedicated thread, this guard's + // `Drop` fires and reports the thread id it ran on. + let _guard = DropSignal { tx: Some(drop_tx) }; + started_tx + .send(std::thread::current().id()) + .expect("started signal must be received"); + // Future returns immediately. The dedicated thread should then + // drop the future (firing _guard), exit the recv loop, and exit. + }); + + let dedicated_thread_id = started_rx + .recv_timeout(Duration::from_secs(2)) + .expect("dedicated future failed to start"); + assert_ne!( + dedicated_thread_id, + std::thread::current().id(), + "dedicated future ran on the test thread" + ); + + // Drive the root task to completion so its body finishes. + futures::executor::block_on(task); + + // The guard's drop runs from the dedicated thread as it tears down + // the future's captured state. If the executor/recv-loop were + // keeping the future alive past task completion, this would hang. + let drop_thread_id = drop_rx + .recv_timeout(Duration::from_secs(2)) + .expect("dedicated future's captured state was not dropped after task completion"); + assert_eq!( + drop_thread_id, dedicated_thread_id, + "dedicated future's captured state must be dropped on the dedicated thread, not elsewhere" + ); + } + + #[test] + fn spawn_dedicated_detached_child_outlives_root() { + use std::sync::mpsc; + + let background = + BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher)))); + + // `gate_rx` lets the detached child park until the test explicitly + // releases it — after we've already observed the root completing. + let (gate_tx, gate_rx) = mpsc::channel::<()>(); + let (child_done_tx, child_done_rx) = mpsc::channel::(); + + let task = background.spawn_dedicated(move |executor| async move { + executor + .spawn(async move { + // Blocking on `recv` is normally wrong inside an + // executor, but the dedicated thread is exclusive to + // this session, so blocking the only future on it is + // fine — this is the property `spawn_dedicated` is + // designed to provide. + gate_rx + .recv() + .expect("gate sender dropped before child resumed"); + child_done_tx + .send(std::thread::current().id()) + .expect("child_done receiver dropped"); + }) + .detach(); + // Root finishes here. The detached child must keep the + // dedicated thread alive until it completes. + }); + + futures::executor::block_on(task); + + // Negative assertion: the child has not finished, because the gate + // hasn't been released yet. + assert!( + child_done_rx + .recv_timeout(Duration::from_millis(50)) + .is_err(), + "detached child finished before being released" + ); + + // Release the gate. The detached child should now complete on the + // dedicated thread. + gate_tx.send(()).expect("gate receiver dropped"); + + let child_thread_id = child_done_rx + .recv_timeout(Duration::from_secs(2)) + .expect("detached child failed to complete after gate was released"); + assert_ne!( + child_thread_id, + std::thread::current().id(), + "detached child ran on the test thread instead of the dedicated thread" + ); + } +} diff --git a/vendor/gpui-pre/src/prelude.rs b/vendor/gpui-pre/src/prelude.rs new file mode 100644 index 0000000..b5185a2 --- /dev/null +++ b/vendor/gpui-pre/src/prelude.rs @@ -0,0 +1,9 @@ +//! The GPUI prelude is a collection of traits and types that are widely used +//! throughout the library. It is recommended to import this prelude into your +//! application to avoid having to import each trait individually. + +pub use crate::{ + AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement, + ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage, + TaskExt as _, VisualContext, util::FluentBuilder, +}; diff --git a/vendor/gpui-pre/src/profiler.rs b/vendor/gpui-pre/src/profiler.rs new file mode 100644 index 0000000..e8108c1 --- /dev/null +++ b/vendor/gpui-pre/src/profiler.rs @@ -0,0 +1,1644 @@ +#[cfg(feature = "profiler")] +use hdrhistogram::Histogram; +use itertools::Itertools; +use scheduler::{Instant, SpawnTime}; +#[cfg(feature = "profiler")] +use smallvec::SmallVec; +use std::{ + cell::LazyCell, + collections::{HashMap, VecDeque}, + hash::{DefaultHasher, Hash, Hasher}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, + thread::ThreadId, + time::Duration, +}; + +mod actions; +#[cfg(feature = "profiler")] +pub mod hang; +#[cfg(feature = "profiler")] +pub mod journal; +pub use actions::{ActionStatistics, ActionTiming, take_action_stats}; + +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "profiler")] +use crate::{Action, App, WindowId}; +use crate::{SharedString, TasksIncluded}; + +#[cfg(feature = "profiler")] +#[doc(hidden)] +pub fn get_all_timings(included: gpui::TasksIncluded) -> Vec { + ThreadTaskTimings::collect(upgraded_thread_timings(), included) +} + +#[cfg(feature = "profiler")] +#[doc(hidden)] +pub fn get_current_thread_timings(included: TasksIncluded) -> gpui::ThreadTaskTimings { + gpui::profiler::get_current_thread_task_timings(included) +} + +#[cfg(feature = "profiler")] +#[doc(hidden)] +pub fn take_all_stats(included: TasksIncluded) -> Vec { + ThreadTaskStatistics::collect_and_reset(upgraded_thread_timings(), included) +} + +#[cfg(not(feature = "profiler"))] +#[doc(hidden)] +pub fn get_all_timings(_included: gpui::TasksIncluded) -> Vec { + Vec::new() +} +#[cfg(not(feature = "profiler"))] +#[doc(hidden)] +pub fn get_current_thread_timings(_included: TasksIncluded) -> gpui::ThreadTaskTimings { + gpui::ThreadTaskTimings { + thread_name: None, + thread_id: std::thread::current().id(), + timings: Vec::new(), + stats: TaskStatistics::default(), + total_pushed: 0, + } +} +#[cfg(not(feature = "profiler"))] +#[doc(hidden)] +pub fn take_all_stats(_included: TasksIncluded) -> Vec { + Vec::new() +} + +#[doc(hidden)] +#[derive(Debug, Copy, Clone)] +pub struct YieldTime(pub Instant); + +#[doc(hidden)] +#[derive(Copy, Clone)] +pub struct TaskTiming { + pub location: &'static core::panic::Location<'static>, + pub spawned: SpawnTime, + pub start: Instant, + pub end: YieldTime, +} + +impl std::fmt::Debug for TaskTiming { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TaskTiming") + .field("location", &self.location) + .field("since_spawned", &self.spawned.0.elapsed()) + .field("last_poll_duration", &self.poll_duration()) + .field("total_runtime", &self.since_spawn()) + .finish() + } +} + +#[doc(hidden)] +#[derive(Debug, Copy, Clone)] +pub struct ActiveTiming { + pub location: &'static core::panic::Location<'static>, + pub spawned: SpawnTime, + pub start: Instant, +} + +impl TaskTiming { + /// A task timing with a duration of zero. Any task will replace this in history. + pub fn placeholder() -> Self { + let now = Instant::now(); + Self { + location: std::panic::Location::caller(), + spawned: SpawnTime(now), + start: now, + end: YieldTime(now), + } + } + + #[inline(always)] + pub fn poll_duration(&self) -> Duration { + self.end.0 - self.start + } + + #[inline(always)] + fn since_spawn(&self) -> Duration { + self.end.0 - self.spawned.0 + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct ThreadTaskTimings { + pub thread_name: Option, + pub thread_id: ThreadId, + pub timings: Vec, + pub stats: TaskStatistics, + pub total_pushed: u64, +} + +impl ThreadTaskTimings { + /// Convert upgraded per-thread timings into their structured format. + pub fn collect( + timings: Vec<(ThreadId, Arc)>, + included: TasksIncluded, + ) -> Vec { + timings + .into_iter() + .map(|(thread_id, timings)| { + let timings = timings.lock(); + let thread_name = timings.thread_name.clone(); + let total_pushed = timings.total_pushed; + let completed = &timings.timings; + + let mut vec = Vec::with_capacity(completed.len() + 1); // +1 for running task + let (s1, s2) = completed.as_slices(); + vec.extend_from_slice(s1); + vec.extend_from_slice(s2); + if let TasksIncluded::CompletedAndRunning = included + && let Some(running) = timings.running + { + vec.push(TaskTiming { + location: running.location, + spawned: running.spawned, + start: running.start, + end: YieldTime(Instant::now()), + }) + } + + ThreadTaskTimings { + thread_name, + thread_id, + timings: vec, + stats: timings.stats.clone(), + total_pushed, + } + }) + .collect() + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct ThreadTaskStatistics { + pub thread_name: Option, + pub thread_id: ThreadId, + pub stats: TaskStatistics, +} + +impl ThreadTaskStatistics { + pub fn collect_and_reset( + timings: Vec<(ThreadId, Arc)>, + include_running: TasksIncluded, + ) -> Vec { + timings + .into_iter() + .map(|(thread_id, timings)| { + let mut timings = timings.lock(); + let thread_name = timings.thread_name.clone(); + + let mut stats = std::mem::take(&mut timings.stats); + if let TasksIncluded::CompletedAndRunning = include_running + && let Some(ActiveTiming { + location, + spawned, + start, + }) = timings.running + { + let end = YieldTime(Instant::now()); + let timing = TaskTiming { + location, + spawned, + start, + end, + }; + stats.add_runtime(timing); + stats.add_yield_timing(timing); + } + + Self { + thread_name, + thread_id, + stats, + } + }) + .collect() + } +} + +/// Serializable variant of [`core::panic::Location`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedLocation { + /// Name of the source file + pub file: SharedString, + /// Line in the source file + pub line: u32, + /// Column in the source file + pub column: u32, +} + +impl From<&core::panic::Location<'static>> for SerializedLocation { + fn from(value: &core::panic::Location<'static>) -> Self { + SerializedLocation { + file: value.file().into(), + line: value.line(), + column: value.column(), + } + } +} + +/// Serializable variant of [`TaskTiming`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedTaskTiming { + /// Location of the timing + pub location: SerializedLocation, + /// Time at which the measurement was reported in nanoseconds + pub start: u128, + /// Duration of the measurement in nanoseconds + pub duration: u128, +} + +impl SerializedTaskTiming { + /// Convert an array of [`TaskTiming`] into their serializable format + /// + /// # Params + /// + /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor + pub fn convert(anchor: Instant, timings: &[TaskTiming]) -> Vec { + let serialized = timings + .iter() + .map(|timing| { + let start = timing.start.duration_since(anchor).as_nanos(); + let duration = timing.end.0.duration_since(timing.start).as_nanos(); + SerializedTaskTiming { + location: timing.location.into(), + start, + duration, + } + }) + .collect::>(); + + serialized + } + + /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor + pub fn from(anchor: Instant, timing: TaskTiming) -> SerializedTaskTiming { + let start = timing.start.duration_since(anchor).as_nanos(); + let duration = timing.end.0.duration_since(timing.start).as_nanos(); + SerializedTaskTiming { + location: timing.location.into(), + start, + duration, + } + } +} + +/// Serializable variant of [`ThreadTaskTimings`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedThreadTaskTimings { + /// Thread name + pub thread_name: Option, + /// Hash of the thread id + pub thread_id: u64, + /// Timing records for this thread + pub timings: Vec, +} + +impl SerializedThreadTaskTimings { + /// Convert [`ThreadTaskTimings`] into their serializable format + /// + /// # Params + /// + /// `anchor` - [`Instant`] that should be earlier than all timings to use as base anchor + pub fn convert(anchor: Instant, timings: ThreadTaskTimings) -> SerializedThreadTaskTimings { + let serialized_timings = SerializedTaskTiming::convert(anchor, &timings.timings); + + let mut hasher = DefaultHasher::new(); + timings.thread_id.hash(&mut hasher); + let thread_id = hasher.finish(); + + SerializedThreadTaskTimings { + thread_name: timings.thread_name, + thread_id, + timings: serialized_timings, + } + } +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct ThreadTimingsDelta { + /// Hashed thread id + pub thread_id: u64, + /// Thread name, if known + pub thread_name: Option, + /// New timings since the last call. If the circular buffer wrapped around + /// since the previous poll, some entries may have been lost. + pub new_timings: Vec, +} + +/// Tracks which timing events have already been seen so that callers can request only unseen events. +#[doc(hidden)] +pub struct ProfilingCollector { + startup_time: Instant, + cursors: HashMap, +} + +impl ProfilingCollector { + pub fn new(startup_time: Instant) -> Self { + Self { + startup_time, + cursors: HashMap::default(), + } + } + + pub fn startup_time(&self) -> Instant { + self.startup_time + } + + pub fn collect_unseen( + &mut self, + all_timings: Vec, + ) -> Vec { + let mut deltas = Vec::with_capacity(all_timings.len()); + + for thread in all_timings { + let mut hasher = DefaultHasher::new(); + thread.thread_id.hash(&mut hasher); + let hashed_id = hasher.finish(); + + let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0); + let buffer_len = thread.timings.len() as u64; + let buffer_start = thread.total_pushed.saturating_sub(buffer_len); + + let mut slice = if prev_cursor < buffer_start { + // Cursor fell behind the buffer — some entries were evicted. + // Return everything still in the buffer. + thread.timings.as_slice() + } else { + let skip = (prev_cursor - buffer_start) as usize; + &thread.timings[skip.min(thread.timings.len())..] + }; + + let cursor_advance = thread.total_pushed; + self.cursors.insert(thread.thread_id, cursor_advance); + + if slice.is_empty() { + continue; + } + + let new_timings = SerializedTaskTiming::convert(self.startup_time, slice); + + deltas.push(ThreadTimingsDelta { + thread_id: hashed_id, + thread_name: thread.thread_name, + new_timings, + }); + } + + deltas + } + + pub fn reset(&mut self) { + self.cursors.clear(); + } +} + +// Allow 16MiB of task timing entries. +// VecDeque grows by doubling its capacity when full, so keep this a power of 2 to avoid wasting +// memory. +#[cfg(feature = "profiler")] +const MAX_TASK_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::(); + +#[doc(hidden)] +pub(crate) type TaskTimings = VecDeque; + +#[doc(hidden)] +pub type GuardedTaskTimings = spin::Mutex; + +#[doc(hidden)] +pub struct GlobalThreadTimings { + pub thread_id: ThreadId, + pub timings: std::sync::Weak, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct TaskStatistics { + pub poll_time_to_beat: Duration, + pub runtime_to_beat: Duration, + pub longest_poll_times: [TaskTiming; 5], + pub longest_runtimes: [TaskTiming; 5], +} + +impl std::fmt::Display for TaskStatistics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Tasks that blocked the longest before yielding\n")?; + for timing in self.longest_poll_times { + f.write_fmt(format_args!( + "{:<20} - {}:{}\n", + format!("{:?}", timing.poll_duration()), + timing.location.file(), + timing.location.column() + ))?; + } + f.write_str("Tasks that ran the longest\n")?; + for timing in self.longest_runtimes { + f.write_fmt(format_args!( + "{:<20} - {}:{}\n", + format!("{:?}", timing.since_spawn()), + timing.location.file(), + timing.location.column() + ))?; + } + Ok(()) + } +} + +impl Default for TaskStatistics { + fn default() -> Self { + Self { + // Do not track polls that are not problematic + // this keeps more calls on the fast path + poll_time_to_beat: Duration::from_micros(100), + runtime_to_beat: Duration::from_micros(100), + longest_poll_times: [TaskTiming::placeholder(); 5], + longest_runtimes: [TaskTiming::placeholder(); 5], + } + } +} + +impl TaskStatistics { + #[inline(always)] + fn add_yield_timing(&mut self, task: TaskTiming) { + let yielded_after = task.poll_duration(); + if yielded_after >= self.poll_time_to_beat { + std::hint::cold_path(); // most tasks are not the worst, optimize for that + let to_replace = self + .longest_poll_times + .iter() + .position_min_by_key(|task| task.since_spawn()) + .expect("guarded by the comparison with nth_longest_yield_time"); + self.longest_poll_times[to_replace] = task; + + self.poll_time_to_beat = self + .longest_poll_times + .iter() + .map(|task| task.since_spawn()) + .min() + .expect("never empty"); + } + } + + #[inline(always)] + fn add_runtime(&mut self, task: TaskTiming) { + let runtime = task.since_spawn(); + if runtime >= self.runtime_to_beat { + std::hint::cold_path(); // most tasks are not the worst, optimize for that + let to_replace = self + .longest_runtimes + .iter() + .position_min_by_key(|task| task.since_spawn()) + .expect("guarded by the comparison with nth_longest_yield_time"); + self.longest_runtimes[to_replace] = task; + + self.runtime_to_beat = self + .longest_runtimes + .iter() + .map(|task| task.since_spawn()) + .min() + .expect("never empty"); + } + } +} + +#[doc(hidden)] +pub static GLOBAL_THREAD_TIMINGS: spin::Mutex> = + spin::Mutex::new(Vec::new()); + +/// Upgrades all live per-thread timing handles, holding the global registry +/// lock only for the duration of the upgrades. +/// +/// The upgraded `Arc`s must never be dropped while `GLOBAL_THREAD_TIMINGS` is +/// locked: dropping the last strong reference runs [`ThreadTimings::drop`], +/// which locks `GLOBAL_THREAD_TIMINGS` again and would deadlock the +/// non-reentrant spinlock. A thread exiting concurrently can hand off its last +/// reference to us at any time, so callers of this function process (lock, +/// read, drop) the returned handles only after the global lock is released. +fn upgraded_thread_timings() -> Vec<(ThreadId, Arc)> { + let global_thread_timings = GLOBAL_THREAD_TIMINGS.lock(); + global_thread_timings + .iter() + .filter_map(|t| Some((t.thread_id, t.timings.upgrade()?))) + .collect() +} + +thread_local! { + #[doc(hidden)] + pub static THREAD_TIMINGS: LazyCell> = LazyCell::new(|| { + let current_thread = std::thread::current(); + let thread_name = current_thread.name(); + let thread_id = current_thread.id(); + let timings = ThreadTimings::new(thread_name.map(|e| e.to_string()), thread_id); + let timings = Arc::new(spin::Mutex::new(timings)); + + { + let timings = Arc::downgrade(&timings); + let global_timings = GlobalThreadTimings { + thread_id: std::thread::current().id(), + timings, + }; + GLOBAL_THREAD_TIMINGS.lock().push(global_timings); + } + + timings + }); +} + +#[doc(hidden)] +pub struct ThreadTimings { + pub thread_name: Option, + pub thread_id: ThreadId, + pub timings: TaskTimings, + pub running: Option, + pub stats: TaskStatistics, + pub total_pushed: u64, +} + +impl ThreadTimings { + pub fn new(thread_name: Option, thread_id: ThreadId) -> Self { + ThreadTimings { + thread_name, + thread_id, + timings: TaskTimings::new(), + stats: TaskStatistics::default(), + total_pushed: 0, + running: None, + } + } + + #[cfg(feature = "profiler")] + pub fn update_running_task( + &mut self, + spawned: SpawnTime, + location: &'static std::panic::Location<'_>, + ) { + let start = Instant::now(); + self.running = Some(ActiveTiming { + spawned, + location, + start, + }); + } + #[cfg(not(feature = "profiler"))] + pub fn update_running_task(&mut self, _: SpawnTime, _: &'static std::panic::Location<'_>) {} + + #[cfg(feature = "profiler")] + pub fn save_task_timing(&mut self, ended: YieldTime) -> TaskTiming { + let ActiveTiming { + location, + start, + spawned, + } = self + .running + .take() + .expect("this function is only ever called after register_task_start"); + + let timing = TaskTiming { + location, + spawned, + start, + end: ended, + }; + self.stats.add_yield_timing(timing); + self.stats.add_runtime(timing); + + if trace_enabled() { + std::hint::cold_path(); // optimize for when the profiling is off + if self.timings.len() >= MAX_TASK_TIMINGS { + self.timings.pop_front(); + } + self.timings.push_back(timing); + self.total_pushed += 1; + } + timing + } + #[cfg(not(feature = "profiler"))] + pub fn save_task_timing(&mut self, _: YieldTime) {} + + // Running tasks are included in the reliability trace, which is written + // whenever the foreground executor makes no progress for > n seconds + pub fn get_thread_task_timings(&self, includes: TasksIncluded) -> ThreadTaskTimings { + ThreadTaskTimings { + thread_name: self.thread_name.clone(), + thread_id: self.thread_id, + timings: self + .timings + .iter() + .cloned() + .chain( + self.running + .filter(|_| matches!(includes, TasksIncluded::CompletedAndRunning)) + .map(|running| TaskTiming { + spawned: running.spawned, + location: running.location, + start: running.start, + end: YieldTime(Instant::now()), + }), + ) + .collect(), + stats: self.stats.clone(), + total_pushed: self.total_pushed, + } + } +} + +impl Drop for ThreadTimings { + fn drop(&mut self) { + let mut thread_timings = GLOBAL_THREAD_TIMINGS.lock(); + + let Some((index, _)) = thread_timings + .iter() + .enumerate() + .find(|(_, t)| t.thread_id == self.thread_id) + else { + return; + }; + thread_timings.swap_remove(index); + } +} + +#[doc(hidden)] +pub fn update_running_task(spawned: SpawnTime, location: &'static std::panic::Location<'_>) { + #[cfg(feature = "profiler")] + journal::begin_foreground_turn(); + THREAD_TIMINGS.with(|timings| { + timings.lock().update_running_task(spawned, location); + }); +} + +#[doc(hidden)] +pub fn save_task_timing() { + let yielded_at = YieldTime(Instant::now()); + #[cfg(feature = "profiler")] + { + let timing = THREAD_TIMINGS.with(|timings| timings.lock().save_task_timing(yielded_at)); + journal::record_task_poll(timing); + } + #[cfg(not(feature = "profiler"))] + THREAD_TIMINGS.with(|timings| { + timings.lock().save_task_timing(yielded_at); + }); +} + +#[doc(hidden)] +pub fn get_current_thread_task_timings(include_running: TasksIncluded) -> ThreadTaskTimings { + THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings(include_running)) +} + +const TRACE_SETTING_ENABLED: u64 = 1 << 63; +const TRACE_SCOPE_COUNT_MASK: u64 = TRACE_SETTING_ENABLED - 1; +static TRACE_STATE: AtomicU64 = AtomicU64::new(0); + +/// Enables or disables profiler trace collection at runtime. +/// +/// When transitioning from enabled to disabled, `add_task_timing` becomes +/// cheaper since only cheap statistics are gathered. The existing per-thread +/// task buffers and the frame-event buffer are cleared so stale data isn't +/// reported after a later re-enable. Active trace scopes keep collection enabled +/// until the last scope ends. Calls with the current setting are a no-op. +pub fn set_trace_enabled(enabled: bool) -> bool { + let mut state = TRACE_STATE.load(Ordering::Acquire); + loop { + let was_enabled = state & TRACE_SETTING_ENABLED != 0; + if was_enabled == enabled { + return false; + } + + let next_state = if enabled { + state | TRACE_SETTING_ENABLED + } else { + state & TRACE_SCOPE_COUNT_MASK + }; + match TRACE_STATE.compare_exchange_weak( + state, + next_state, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + if next_state == 0 { + clear_trace_buffers(); + } + return true; + } + Err(updated_state) => state = updated_state, + } + } +} + +#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] +pub(crate) struct TraceGuard; + +#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] +pub(crate) fn trace_scope() -> TraceGuard { + let incremented = TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| { + (state & TRACE_SCOPE_COUNT_MASK < TRACE_SCOPE_COUNT_MASK).then_some(state + 1) + }); + assert!(incremented.is_ok(), "too many active profiler trace scopes"); + TraceGuard +} + +#[cfg(any(feature = "bench-support", all(test, feature = "profiler")))] +impl Drop for TraceGuard { + fn drop(&mut self) { + let previous_state = + TRACE_STATE.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| { + (state & TRACE_SCOPE_COUNT_MASK > 0).then_some(state - 1) + }); + match previous_state { + Ok(1) => clear_trace_buffers(), + Ok(_) => {} + Err(_) => debug_assert!(false, "profiler trace scope count underflowed"), + } + } +} + +/// Returns whether profiler trace collection is enabled. +pub fn trace_enabled() -> bool { + TRACE_STATE.load(Ordering::Relaxed) != 0 +} + +fn clear_trace_buffers() { + for (_, timings) in upgraded_thread_timings() { + let mut timings = timings.lock(); + timings.timings.clear(); + timings.timings.shrink_to_fit(); + timings.total_pushed = 0; + } + #[cfg(feature = "profiler")] + { + let mut frames = FRAME_TIMINGS.lock(); + frames.timings.clear(); + frames.timings.shrink_to_fit(); + frames.total_pushed = 0; + } +} + +/// Timing for a single drawn window frame. +#[cfg(feature = "profiler")] +#[derive(Debug, Copy, Clone)] +pub struct FrameTiming { + /// The window that was drawn. + pub window_id: WindowId, + /// When the frame first became dirty (its first invalidation). `None` if + /// profiler tracing was not yet enabled when the invalidation occurred. + pub dirty_at: Option, + /// Number of invalidations coalesced into this frame. + pub invalidations: u64, + /// When `Window::draw` started. + pub draw_start: Instant, + /// When `Window::draw` finished. + pub draw_end: Instant, +} + +#[cfg(feature = "profiler")] +impl FrameTiming { + /// Time spent inside `Window::draw`. + pub fn draw_duration(&self) -> Duration { + self.draw_end.duration_since(self.draw_start) + } + + /// Time from the frame's first invalidation to the end of its draw, if the + /// first invalidation was observed. + pub fn dirty_to_draw_duration(&self) -> Option { + self.dirty_at + .map(|dirty_at| self.draw_end.duration_since(dirty_at)) + } +} + +/// Work spent submitting a window frame to the platform. +#[cfg(feature = "profiler")] +#[derive(Debug, Copy, Clone)] +pub struct PresentTiming { + /// The window whose frame was submitted. + pub window_id: WindowId, + /// When the platform submission began. + pub present_start: Instant, + /// When the platform submission completed. + pub present_end: Instant, + /// The interval since the previous newly drawn frame was submitted, when + /// both frames belong to an active animation. + pub animation_interval: Option, +} + +#[cfg(feature = "profiler")] +impl PresentTiming { + /// Time spent submitting the frame to the platform. + pub fn present_duration(&self) -> Duration { + self.present_end.duration_since(self.present_start) + } +} + +/// A frame event observed by the profiler. +#[cfg(feature = "profiler")] +#[derive(Debug, Copy, Clone)] +pub enum FrameEvent { + /// A window frame was drawn. + Draw(FrameTiming), + /// A newly drawn window frame was presented. + Present(PresentTiming), +} + +/// A point-in-time snapshot of the frame-duration histograms for a window, +/// suitable for external formatting. +#[cfg(feature = "profiler")] +#[derive(Clone)] +pub struct FrameDurationSnapshot { + /// Histogram of durations from the first invalidation through presentation, in nanoseconds. + pub dirty_to_present_histogram: Histogram, + /// Histogram of `Window::draw` durations, in nanoseconds. + pub draw_duration_histogram: Histogram, + /// Histogram of intervals between consecutively presented frames while the + /// window was animating, in nanoseconds. + pub present_interval_histogram: Histogram, +} + +/// A point-in-time snapshot of the input-latency histograms for a window, +/// suitable for external formatting. +#[cfg(feature = "profiler")] +#[derive(Clone)] +pub struct InputLatencySnapshot { + /// Histogram of input-to-frame latency samples, in nanoseconds. + pub latency_histogram: Histogram, + /// Histogram of input events coalesced per rendered frame. + pub events_per_frame_histogram: Histogram, + /// Count of input events that arrived mid-draw and were excluded from + /// latency recording. + pub mid_draw_events_dropped: u64, +} + +#[cfg(feature = "profiler")] +enum WindowActivity { + Input { + started_at: Instant, + kind: &'static str, + }, + Draw { + started_at: Instant, + }, +} + +/// Collects profiling information for one window. +/// +/// Aggregate histograms are always populated when the `profiler` feature is +/// compiled in. Individual draw and present events are added to the global +/// profiler buffer only while tracing is enabled. +#[cfg(feature = "profiler")] +pub struct WindowProfiler { + window_id: WindowId, + active_activities: SmallVec<[WindowActivity; 4]>, + active_actions: SmallVec<[(&'static str, Instant); 2]>, + dirty_to_present_histogram: Histogram, + draw_duration_histogram: Histogram, + present_interval_histogram: Histogram, + first_input_at: Option, + pending_input_count: u64, + input_latency_histogram: Histogram, + events_per_frame_histogram: Histogram, + mid_draw_events_dropped: u64, + last_present_at: Option, + animating_at_last_present: bool, + pending_frame: Option, +} + +#[cfg(feature = "profiler")] +impl WindowProfiler { + /// Creates a profiler for a window. + pub fn new(window_id: WindowId) -> anyhow::Result { + let profiler = Self { + window_id, + active_activities: SmallVec::new(), + active_actions: SmallVec::new(), + dirty_to_present_histogram: Histogram::new(3).map_err(|error| { + anyhow::anyhow!("Failed to create dirty-to-present histogram: {error}") + })?, + draw_duration_histogram: Histogram::new(3).map_err(|error| { + anyhow::anyhow!("Failed to create draw duration histogram: {error}") + })?, + present_interval_histogram: Histogram::new(3).map_err(|error| { + anyhow::anyhow!("Failed to create present interval histogram: {error}") + })?, + first_input_at: None, + pending_input_count: 0, + input_latency_histogram: Histogram::new(3).map_err(|error| { + anyhow::anyhow!("Failed to create input latency histogram: {error}") + })?, + events_per_frame_histogram: Histogram::new(3).map_err(|error| { + anyhow::anyhow!("Failed to create events per frame histogram: {error}") + })?, + mid_draw_events_dropped: 0, + last_present_at: None, + animating_at_last_present: false, + pending_frame: None, + }; + journal::record_frame_pending(window_id, Instant::now()); + Ok(profiler) + } + + /// Records the beginning of an input dispatch. `kind` names the platform + /// input variant being dispatched (see [`crate::PlatformInput::kind_name`]). + pub fn begin_input(&mut self, kind: &'static str) { + journal::begin_foreground_turn(); + self.active_activities.push(WindowActivity::Input { + started_at: Instant::now(), + kind, + }); + } + + /// Records the end of an input dispatch. + pub fn end_input(&mut self, caused_invalidation: bool) { + let Some(WindowActivity::Input { started_at, kind }) = self.active_activities.pop() else { + debug_assert!(false, "input activity must be the current window activity"); + journal::end_foreground_turn(); + return; + }; + + if !journal::power_interrupted_since(started_at) { + journal::record_input(journal::InputTiming { + kind, + start: started_at, + end: Instant::now(), + caused_invalidation, + }); + } + journal::end_foreground_turn(); + + if !caused_invalidation || !journal::frame_sample_is_valid(self.window_id, started_at) { + return; + } + if self + .first_input_at + .is_some_and(|at| !journal::frame_sample_is_valid(self.window_id, at)) + { + self.first_input_at = None; + self.pending_input_count = 0; + } + + let arrived_during_draw = self + .active_activities + .iter() + .any(|activity| matches!(activity, WindowActivity::Draw { .. })); + if arrived_during_draw { + self.mid_draw_events_dropped += 1; + } else { + self.first_input_at.get_or_insert(started_at); + self.pending_input_count += 1; + } + } + + /// Records the beginning of an action handler. + pub fn begin_action_handler(&mut self, action: &(dyn Action + 'static), cx: &mut App) { + journal::begin_foreground_turn(); + let name = actions::update_running_action(action, cx); + self.active_actions.push((name, Instant::now())); + } + + /// Records the end of the current action handler. + pub fn end_action_handler(&mut self) { + // Dual-write to the legacy aggregate store; its single global running + // slot misbehaves when tests run actions concurrently, which is why + // the journal entry is tracked here on the window instead. + actions::save_action_timing(); + let Some((name, start)) = self.active_actions.pop() else { + debug_assert!(false, "action handler must be begun before it ends"); + journal::end_foreground_turn(); + return; + }; + if !journal::power_interrupted_since(start) { + journal::record_action(ActionTiming { + name, + start, + end: Instant::now(), + }); + } + journal::end_foreground_turn(); + } + + /// Records the beginning of a window draw. + pub fn begin_draw(&mut self) { + journal::begin_foreground_turn(); + let started_at = Instant::now(); + journal::record_frame_pending(self.window_id, started_at); + self.active_activities + .push(WindowActivity::Draw { started_at }); + } + + /// Records the end of a window draw and returns the draw duration. + pub fn end_draw(&mut self, dirty_at: Option, invalidations: u64) -> Duration { + let Some(WindowActivity::Draw { + started_at: draw_start, + }) = self.active_activities.pop() + else { + debug_assert!(false, "draw activity must be the current window activity"); + journal::end_foreground_turn(); + return Duration::ZERO; + }; + + let draw_end = Instant::now(); + let frame_timing = FrameTiming { + window_id: self.window_id, + dirty_at: dirty_at.filter(|at| journal::frame_sample_is_valid(self.window_id, *at)), + invalidations, + draw_start, + draw_end, + }; + let draw_duration = frame_timing.draw_duration(); + if !journal::power_interrupted_since(draw_start) { + self.record_draw_timing(frame_timing); + } + journal::end_foreground_turn(); + draw_duration + } + + /// Records that a frame was presented. + /// + /// `next_frame_scheduled` marks the animation state for the interval ending + /// at the next newly drawn frame's presentation. + pub fn record_present( + &mut self, + present_start: Instant, + present_end: Instant, + window_active: bool, + next_frame_scheduled: bool, + ) { + self.record_present_at( + present_start, + present_end, + window_active, + next_frame_scheduled, + ); + } + + /// Returns a snapshot of the current input-latency histograms. + pub fn input_latency_snapshot(&self) -> InputLatencySnapshot { + InputLatencySnapshot { + latency_histogram: self.input_latency_histogram.clone(), + events_per_frame_histogram: self.events_per_frame_histogram.clone(), + mid_draw_events_dropped: self.mid_draw_events_dropped, + } + } + + /// Returns a snapshot of the current frame-duration histograms. + pub fn frame_duration_snapshot(&self) -> FrameDurationSnapshot { + FrameDurationSnapshot { + dirty_to_present_histogram: self.dirty_to_present_histogram.clone(), + draw_duration_histogram: self.draw_duration_histogram.clone(), + present_interval_histogram: self.present_interval_histogram.clone(), + } + } + + fn record_present_at( + &mut self, + present_start: Instant, + present_end: Instant, + window_active: bool, + next_frame_scheduled: bool, + ) { + if let Some(first_input_at) = self.first_input_at.take() + && journal::frame_sample_is_valid(self.window_id, first_input_at) + { + let latency_nanos = present_end.duration_since(first_input_at).as_nanos() as u64; + self.input_latency_histogram.record(latency_nanos).ok(); + if self.pending_input_count > 0 { + self.events_per_frame_histogram + .record(self.pending_input_count) + .ok(); + } + } + self.pending_input_count = 0; + + let frame = self + .pending_frame + .take() + .filter(|frame| journal::frame_sample_is_valid(self.window_id, frame.draw_start)) + .map(|mut frame| { + frame.dirty_at = frame + .dirty_at + .filter(|at| journal::frame_sample_is_valid(self.window_id, *at)); + frame + }); + let animation_interval = + if frame.is_some() && self.animating_at_last_present && window_active { + self.last_present_at + .filter(|at| journal::frame_sample_is_valid(self.window_id, *at)) + .map(|last_present_at| present_end.duration_since(last_present_at)) + } else { + None + }; + let present_timing = PresentTiming { + window_id: self.window_id, + present_start, + present_end, + animation_interval, + }; + journal::record_present(present_timing, frame); + + let Some(frame) = frame else { + return; + }; + + if let Some(dirty_at) = frame.dirty_at + && let Err(error) = self + .dirty_to_present_histogram + .record(present_end.duration_since(dirty_at).as_nanos() as u64) + { + log::error!("failed to record dirty-to-present frame timing: {error}"); + } + + if let Some(animation_interval) = animation_interval { + self.present_interval_histogram + .record(animation_interval.as_nanos() as u64) + .ok(); + } + record_frame_event(FrameEvent::Present(present_timing)); + + self.last_present_at = Some(present_end); + self.animating_at_last_present = next_frame_scheduled && window_active; + } + + fn record_draw_timing(&mut self, timing: FrameTiming) { + self.record_draw_duration(timing.draw_duration()); + self.pending_frame = Some(timing); + record_frame_event(FrameEvent::Draw(timing)); + journal::record_draw(timing); + } + + fn record_draw_duration(&mut self, duration: Duration) { + self.draw_duration_histogram + .record(duration.as_nanos() as u64) + .ok(); + } +} + +#[cfg(feature = "profiler")] +impl Drop for WindowProfiler { + fn drop(&mut self) { + journal::record_window_closed(self.window_id); + } +} + +// Allow 16MiB of frame event entries. +#[cfg(feature = "profiler")] +const MAX_FRAME_TIMINGS: usize = (16 * 1024 * 1024) / core::mem::size_of::(); + +#[cfg(feature = "profiler")] +struct FrameTimings { + timings: VecDeque, + total_pushed: u64, +} + +#[cfg(feature = "profiler")] +static FRAME_TIMINGS: spin::Mutex = spin::Mutex::new(FrameTimings { + timings: VecDeque::new(), + total_pushed: 0, +}); + +/// Records a frame event. +/// +/// No-op unless profiler tracing is enabled via [`set_trace_enabled`]. +#[cfg(feature = "profiler")] +pub fn record_frame_event(event: FrameEvent) { + if !trace_enabled() { + return; + } + std::hint::cold_path(); // optimize for when profiling is off + + let mut frames = FRAME_TIMINGS.lock(); + if frames.timings.len() >= MAX_FRAME_TIMINGS { + frames.timings.pop_front(); + } + frames.timings.push_back(event); + frames.total_pushed += 1; +} + +/// Drains frame events recorded after this collector was created, tracking a +/// cursor so each call to [`Self::collect_unseen`] returns only new entries. +#[cfg(feature = "profiler")] +pub struct FrameTimingCollector { + cursor: u64, +} + +#[cfg(feature = "profiler")] +impl Default for FrameTimingCollector { + fn default() -> Self { + Self::new() + } +} + +#[cfg(feature = "profiler")] +impl FrameTimingCollector { + /// Creates a collector that only sees frame events recorded from this point on. + pub fn new() -> Self { + Self { + cursor: FRAME_TIMINGS.lock().total_pushed, + } + } + + /// Returns frame events recorded since the previous call (or since the + /// collector was created). If the ring buffer wrapped around since the + /// previous poll, the evicted entries are lost. + pub fn collect_unseen(&mut self) -> Vec { + let frames = FRAME_TIMINGS.lock(); + let buffer_len = frames.timings.len() as u64; + let buffer_start = frames.total_pushed.saturating_sub(buffer_len); + let skip = self.cursor.saturating_sub(buffer_start) as usize; + let unseen = frames + .timings + .iter() + .skip(skip.min(frames.timings.len())) + .copied() + .collect(); + self.cursor = frames.total_pushed; + unseen + } +} + +#[cfg(all(test, feature = "profiler"))] +mod tests { + use super::*; + use std::sync::{Mutex, MutexGuard}; + + #[test] + fn interruptions_drop_latency_samples_but_hiding_keeps_draw_work() { + for sleep in [false, true] { + let (_journal, _guard) = journal::install_test_foreground_journal(64, 4); + let id = WindowId::from(1); + let mut profiler = WindowProfiler::new(id).expect("valid histograms"); + let old = Instant::now() - Duration::from_secs(1); + record_test_draw(&mut profiler, old); + profiler.record_present_at(old, old, true, true); + profiler.first_input_at = Some(old); + profiler.pending_input_count = 1; + profiler.begin_draw(); + if sleep { + journal::record_power_transition(journal::PowerState::Suspended); + journal::record_power_transition(journal::PowerState::Awake); + } else { + journal::record_window_visibility(id, crate::WindowVisibility::Hidden); + journal::record_window_visibility(id, crate::WindowVisibility::Visible); + } + profiler.end_draw(Some(old), 1); + let now = Instant::now(); + profiler.record_present_at(now, now, true, true); + assert_eq!( + profiler.draw_duration_histogram.len(), + if sleep { 1 } else { 2 } + ); + assert_eq!(profiler.input_latency_histogram.len(), 0); + assert_eq!(profiler.dirty_to_present_histogram.len(), 1); + assert_eq!(profiler.present_interval_histogram.len(), 0); + profiler.begin_draw(); + profiler.end_draw(Some(Instant::now()), 1); + let now = Instant::now(); + profiler.record_present_at(now, now, true, true); + assert_eq!(profiler.dirty_to_present_histogram.len(), 2); + } + } + + #[test] + fn records_draw_events_only_while_tracing() { + let _trace_test_guard = TraceTestGuard::new(); + let window_id = WindowId::from(0xD0A0); + let mut window_profiler = + WindowProfiler::new(window_id).expect("window profiler should initialize"); + let dirty_at = Instant::now(); + let mut collector = FrameTimingCollector::new(); + + window_profiler.begin_draw(); + window_profiler.end_draw(Some(dirty_at), 3); + assert!( + collector + .collect_unseen() + .iter() + .all(|event| !event_matches_window(*event, window_id)) + ); + + set_trace_enabled(true); + let mut collector = FrameTimingCollector::new(); + window_profiler.begin_draw(); + window_profiler.end_draw(Some(dirty_at), 3); + + let timing = collector + .collect_unseen() + .into_iter() + .find_map(|event| match event { + FrameEvent::Draw(timing) if timing.window_id == window_id => Some(timing), + _ => None, + }) + .expect("draw event should be recorded while tracing"); + assert_eq!(timing.dirty_at, Some(dirty_at)); + assert_eq!(timing.invalidations, 3); + assert!(timing.draw_start >= dirty_at); + } + + #[test] + fn records_present_events_for_newly_drawn_frames() { + let _trace_test_guard = TraceTestGuard::new(); + set_trace_enabled(true); + let window_id = WindowId::from(0xA11E); + let mut window_profiler = + WindowProfiler::new(window_id).expect("window profiler should initialize"); + let start = Instant::now(); + let mut collector = FrameTimingCollector::new(); + + record_test_draw(&mut window_profiler, start); + window_profiler.record_present_at(start, start, true, true); + record_test_draw(&mut window_profiler, start + FRAME); + window_profiler.record_present_at(start + FRAME, start + FRAME, true, true); + window_profiler.record_present_at( + start + FRAME + FRAME / 2, + start + FRAME + FRAME / 2, + true, + true, + ); + + let present_timings = collector + .collect_unseen() + .into_iter() + .filter_map(|event| match event { + FrameEvent::Present(timing) if timing.window_id == window_id => Some(timing), + _ => None, + }) + .collect::>(); + let [first_present, second_present] = present_timings.as_slice() else { + panic!("expected exactly two present events, got {present_timings:?}"); + }; + assert_eq!(first_present.animation_interval, None); + assert_eq!(second_present.animation_interval, Some(FRAME)); + + #[cfg(feature = "profiler")] + { + assert_eq!(window_profiler.present_interval_histogram.len(), 1); + assert!( + window_profiler.present_interval_histogram.max() + >= second_present + .animation_interval + .expect("second present should have an animation interval") + .as_nanos() as u64 + ); + } + } + + #[test] + fn disabling_tracing_clears_frame_events() { + let _trace_test_guard = TraceTestGuard::new(); + set_trace_enabled(true); + let window_id = WindowId::from(0xC1EA); + let mut window_profiler = + WindowProfiler::new(window_id).expect("window profiler should initialize"); + let mut collector = FrameTimingCollector::new(); + + window_profiler.begin_draw(); + window_profiler.end_draw(None, 0); + assert!( + FRAME_TIMINGS + .lock() + .timings + .iter() + .copied() + .any(|event| event_matches_window(event, window_id)) + ); + + set_trace_enabled(false); + assert!( + collector + .collect_unseen() + .iter() + .all(|event| !event_matches_window(*event, window_id)) + ); + } + + #[cfg(feature = "profiler")] + #[test] + fn records_intervals_only_between_animation_frames() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(1)).expect("window profiler should initialize"); + let start = Instant::now(); + + draw_and_present(&mut window_profiler, start, true, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 0); + + draw_and_present(&mut window_profiler, start + FRAME, true, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 1); + + draw_and_present(&mut window_profiler, start + FRAME * 2, true, false); + assert_eq!(window_profiler.present_interval_histogram.len(), 2); + + draw_and_present(&mut window_profiler, start + FRAME * 100, true, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 2); + } + + #[cfg(feature = "profiler")] + #[test] + fn missed_frames_stretch_the_recorded_interval() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(2)).expect("window profiler should initialize"); + let start = Instant::now(); + + draw_and_present(&mut window_profiler, start, true, true); + draw_and_present(&mut window_profiler, start + FRAME * 5, true, true); + + let recorded = window_profiler.present_interval_histogram.max(); + assert!(recorded >= (FRAME * 4).as_nanos() as u64); + } + + #[cfg(feature = "profiler")] + #[test] + fn ignores_re_presents_of_unchanged_frames() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(3)).expect("window profiler should initialize"); + let start = Instant::now(); + + draw_and_present(&mut window_profiler, start, true, true); + window_profiler.record_present_at(start + FRAME / 2, start + FRAME / 2, true, true); + draw_and_present(&mut window_profiler, start + FRAME, true, true); + + assert_eq!(window_profiler.present_interval_histogram.len(), 1); + assert!( + window_profiler.present_interval_histogram.max() >= (FRAME * 3 / 4).as_nanos() as u64 + ); + } + + #[cfg(feature = "profiler")] + #[test] + fn skips_intervals_for_inactive_windows() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(4)).expect("window profiler should initialize"); + let start = Instant::now(); + + draw_and_present(&mut window_profiler, start, false, true); + draw_and_present(&mut window_profiler, start + FRAME, false, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 0); + + draw_and_present(&mut window_profiler, start + FRAME * 2, true, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 0); + + draw_and_present(&mut window_profiler, start + FRAME * 3, true, true); + assert_eq!(window_profiler.present_interval_histogram.len(), 1); + } + + #[test] + fn records_dirty_to_present_durations() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(8)).expect("window profiler should initialize"); + let draw_end = Instant::now(); + let present_end = draw_end + Duration::from_millis(6); + + record_test_draw(&mut window_profiler, draw_end); + window_profiler.record_present_at(present_end, present_end, true, false); + + let snapshot = window_profiler.frame_duration_snapshot(); + let histogram = snapshot.dirty_to_present_histogram; + assert_eq!(histogram.len(), 1); + assert!(histogram.max() >= Duration::from_millis(10).as_nanos() as u64); + } + + #[cfg(feature = "profiler")] + #[test] + fn records_every_draw_duration() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(5)).expect("window profiler should initialize"); + + window_profiler.record_draw_duration(Duration::from_millis(2)); + window_profiler.record_draw_duration(Duration::from_millis(40)); + + let snapshot = window_profiler.frame_duration_snapshot(); + assert_eq!(snapshot.draw_duration_histogram.len(), 2); + assert!(snapshot.draw_duration_histogram.max() >= 39_000_000); + } + + #[test] + fn records_input_latency_at_the_frame_presentation_timestamp() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(6)).expect("window profiler should initialize"); + let first_input_at = Instant::now(); + let presented_at = first_input_at + Duration::from_millis(12); + + begin_input_at(&mut window_profiler, first_input_at); + window_profiler.end_input(true); + begin_input_at( + &mut window_profiler, + first_input_at + Duration::from_millis(2), + ); + window_profiler.end_input(true); + record_test_draw(&mut window_profiler, presented_at); + window_profiler.record_present_at(presented_at, presented_at, true, false); + + let snapshot = window_profiler.input_latency_snapshot(); + assert_eq!(snapshot.latency_histogram.len(), 1); + assert!(snapshot.latency_histogram.max() >= Duration::from_millis(12).as_nanos() as u64); + assert_eq!(snapshot.events_per_frame_histogram.len(), 1); + assert_eq!(snapshot.events_per_frame_histogram.max(), 2); + assert_eq!(snapshot.mid_draw_events_dropped, 0); + } + + #[test] + fn excludes_input_that_arrives_during_a_draw() { + let mut window_profiler = + WindowProfiler::new(WindowId::from(7)).expect("window profiler should initialize"); + + window_profiler.begin_draw(); + begin_input_at(&mut window_profiler, Instant::now()); + window_profiler.end_input(true); + window_profiler.end_draw(None, 0); + + let snapshot = window_profiler.input_latency_snapshot(); + assert!(snapshot.latency_histogram.is_empty()); + assert!(snapshot.events_per_frame_histogram.is_empty()); + assert_eq!(snapshot.mid_draw_events_dropped, 1); + } + + #[test] + fn overlapping_trace_scopes_keep_tracing_enabled() { + let _trace_test_guard = TraceTestGuard::new(); + let first_scope = trace_scope(); + let second_scope = trace_scope(); + + assert!(trace_enabled()); + drop(first_scope); + assert!(trace_enabled()); + drop(second_scope); + assert!(!trace_enabled()); + } + + const FRAME: Duration = Duration::from_millis(16); + static TRACE_TEST_LOCK: Mutex<()> = Mutex::new(()); + + struct TraceTestGuard { + was_enabled: bool, + _lock: MutexGuard<'static, ()>, + } + + impl TraceTestGuard { + fn new() -> Self { + let lock = TRACE_TEST_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let was_enabled = trace_enabled(); + set_trace_enabled(false); + Self { + was_enabled, + _lock: lock, + } + } + } + + impl Drop for TraceTestGuard { + fn drop(&mut self) { + set_trace_enabled(false); + if self.was_enabled { + set_trace_enabled(true); + } + } + } + + fn event_matches_window(event: FrameEvent, window_id: WindowId) -> bool { + match event { + FrameEvent::Draw(timing) => timing.window_id == window_id, + FrameEvent::Present(timing) => timing.window_id == window_id, + } + } + + fn begin_input_at(window_profiler: &mut WindowProfiler, started_at: Instant) { + window_profiler + .active_activities + .push(WindowActivity::Input { + started_at, + kind: "test", + }); + } + + #[cfg(feature = "profiler")] + fn draw_and_present( + window_profiler: &mut WindowProfiler, + presented_at: Instant, + window_active: bool, + next_frame_scheduled: bool, + ) { + record_test_draw(window_profiler, presented_at); + window_profiler.record_present_at( + presented_at, + presented_at, + window_active, + next_frame_scheduled, + ); + } + + fn record_test_draw(window_profiler: &mut WindowProfiler, draw_end: Instant) { + window_profiler.record_draw_timing(FrameTiming { + window_id: window_profiler.window_id, + dirty_at: Some(draw_end - Duration::from_millis(4)), + invalidations: 1, + draw_start: draw_end - Duration::from_millis(2), + draw_end, + }); + } +} diff --git a/vendor/gpui-pre/src/profiler/actions.rs b/vendor/gpui-pre/src/profiler/actions.rs new file mode 100644 index 0000000..a4892f8 --- /dev/null +++ b/vendor/gpui-pre/src/profiler/actions.rs @@ -0,0 +1,213 @@ +use std::time::Duration; + +use itertools::Itertools; +use scheduler::Instant; + +#[cfg(feature = "profiler")] +use crate::action::Action; + +#[doc(hidden)] +#[derive(Clone)] +pub struct ActionStatistics { + runtime_to_beat: Duration, + + longest_runtimes: heapless::Vec, + running: Option<(&'static str, Instant)>, +} + +impl std::fmt::Debug for ActionStatistics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActionStatistics") + .field("runtime_to_beat", &self.runtime_to_beat) + .field("longest_runtimes", &self.longest_runtimes) + .field( + "running", + &self.running.map(|(id, started)| (id, started.elapsed())), + ) + .finish() + } +} + +impl std::fmt::Display for ActionStatistics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Actions that blocked the longest\n")?; + for action in self + .longest_runtimes(true) + .sorted_by_key(|action| action.runtime()) + .rev() + { + f.write_fmt(format_args!( + "{:<20} - {}", + format!("{:?}", action.runtime()), // impl dbg does not support alignment + action.name + ))?; + writeln!(f)?; + } + Ok(()) + } +} + +impl Default for ActionStatistics { + fn default() -> Self { + Self::new() + } +} + +impl ActionStatistics { + const fn new() -> Self { + Self { + // This keeps more calls on the fast path by only tracking + // problematic polls + runtime_to_beat: Duration::from_micros(100), + longest_runtimes: heapless::Vec::new(), + running: None, + } + } + + pub fn take(&mut self) -> Self { + let taken = std::mem::take(self); + self.running = taken.running; + taken + } + + pub fn is_empty(&self) -> bool { + self.longest_runtimes.is_empty() + } + + #[cfg(feature = "profiler")] + pub fn update_running_action(&mut self, action: &'static str, started: Instant) { + self.running = Some((action, started)); + } + #[cfg(not(feature = "profiler"))] + pub fn update_running_action(&mut self, _action: &'static str, _started: Instant) {} + + #[cfg(feature = "profiler")] + pub fn save_action_timing(&mut self) { + let now = Instant::now(); + + let Some((action, started)) = self.running.take() else { + // Actions are ran only on the foreground executor and therefore + // sequentially _except_ in tests where they can run concurrently. + // + // When ran sequentially self.running will always be Some. When ran + // concurrently that is no longer true. But that is fine, we do not + // need to track action timings in tests. + std::hint::cold_path(); + return; + }; + + let timing = ActionTiming { + name: action, + start: started, + end: now, + }; + + let runtime = now.duration_since(started); + if runtime >= self.runtime_to_beat { + std::hint::cold_path(); // most actions are not the worst, optimize for that + + if self.longest_runtimes.is_full() + && let Some(to_replace) = self + .longest_runtimes + .iter_mut() + .min_by_key(|action| runtime >= action.runtime()) + { + *to_replace = timing; + } else { + self.longest_runtimes + .push(timing) + .expect("just checked it is not full"); + }; + + self.runtime_to_beat = self + .longest_runtimes + .iter() + .map(|action| action.runtime()) + .min() + .expect("never empty"); + } + } + #[cfg(not(feature = "profiler"))] + pub fn save_action_timing(&mut self) {} + + pub fn longest_runtimes(&self, include_running: bool) -> impl Iterator { + self.longest_runtimes.iter().copied().chain( + self.running + .into_iter() + .filter(move |_| include_running) + .map(|(name, start)| ActionTiming { + name, + start, + end: Instant::now(), + }), + ) + } +} + +#[doc(hidden)] +/// UNSTABLE only for use in the profiler and zed-reliability +#[derive(Copy, Clone)] +pub struct ActionTiming { + pub name: &'static str, + pub start: Instant, + pub end: Instant, +} + +impl core::fmt::Debug for ActionTiming { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActionTiming") + .field("name", &self.name) + .field("runtime", &self.runtime()) + .finish() + } +} + +impl ActionTiming { + pub fn duration(&self) -> Duration { + self.end.saturating_duration_since(self.start) + } +} + +impl ActionTiming { + #[doc(hidden)] + pub fn runtime(&self) -> Duration { + self.end - self.start + } +} + +// The profiler is careful to never block when the lock is held, therefore a +// spinlock is optimal. +#[cfg(feature = "profiler")] +static ACTION_STATISTICS: spin::Mutex = + const { spin::Mutex::new(ActionStatistics::new()) }; + +#[doc(hidden)] +#[cfg(feature = "profiler")] +pub(crate) fn update_running_action( + action: &(dyn Action + 'static), + cx: &mut crate::App, +) -> &'static str { + let now = Instant::now(); + let action = action.type_id(); + let action = cx.actions.try_resolve_action(&action).unwrap_or("un-named"); + ACTION_STATISTICS.lock().update_running_action(action, now); + action +} + +#[doc(hidden)] +#[cfg(feature = "profiler")] +pub(crate) fn save_action_timing() { + ACTION_STATISTICS.lock().save_action_timing(); +} + +#[doc(hidden)] +#[cfg(feature = "profiler")] +pub fn take_action_stats() -> ActionStatistics { + ACTION_STATISTICS.lock().take() +} + +#[doc(hidden)] +#[cfg(not(feature = "profiler"))] +pub fn take_action_stats() -> ActionStatistics { + ActionStatistics::default() +} diff --git a/vendor/gpui-pre/src/profiler/hang.rs b/vendor/gpui-pre/src/profiler/hang.rs new file mode 100644 index 0000000..fbd5234 --- /dev/null +++ b/vendor/gpui-pre/src/profiler/hang.rs @@ -0,0 +1,1506 @@ +//! Post-hoc detection of foreground hangs from the journal's event stream. +//! +//! A hang is any single piece of foreground work — a task poll, an action +//! handler, an input dispatch, a window draw, or platform presentation — that +//! blocked the foreground thread for at least a threshold duration. An +//! interval whose total foreground spend reached the frame budget also +//! counts, even when no single event crossed the threshold: many small +//! pieces of work can drop a frame — or starve a headless app — as +//! thoroughly as one long stall. [`HangDetector`] drains the journal and +//! reports each completed activity interval containing hangs as a +//! [`HangIncident`]. + +use std::time::Duration; + +use scheduler::Instant; +use serde::Serialize; + +#[cfg(not(target_family = "wasm"))] +mod monitor; +#[cfg(not(target_family = "wasm"))] +pub(crate) use monitor::HangMonitor; +#[cfg(not(target_family = "wasm"))] +pub use monitor::{HangMonitorConfig, HangMonitorError, HangMonitorPoll, HangMonitorPollReason}; + +/// Version of the power/visibility-aware measurement rules. +pub const MEASUREMENT_VERSION: u32 = 2; + +use super::SerializedLocation; +use super::journal::{ + ForegroundEvent, ForegroundJournal, ForegroundJournalCollector, ForegroundJournalEntry, + FrameSnapshot, IntervalBoundary, IntervalSealer, +}; + +/// Detects foreground hangs by polling the journal. +/// +/// Detection is post-hoc: a hang is reported once an explicit presentation, +/// completed frame skip, or foreground-idle boundary completes its interval. +/// Work that never yields back to the foreground is not observed until it does. +pub struct HangDetector { + collector: ForegroundJournalCollector, + sealer: IntervalSealer, + threshold: Duration, + frame_budget: Duration, + first_present_at: Option, +} + +/// One sealed interval that contained at least one hang. +#[derive(Debug, Clone)] +pub struct HangIncident { + /// The interval the hangs occurred in, including all non-hang foreground + /// work recorded alongside them. + pub snapshot: FrameSnapshot, + /// Which detection rule qualified the interval. + pub trigger: HangTrigger, + /// For [`HangTrigger::Threshold`], the events that blocked the foreground + /// for at least the detector's threshold, longest first. For + /// [`HangTrigger::Budget`], no event crossed the threshold and this instead + /// holds every event in the interval, longest first. + pub contributors: Vec, +} + +/// The detection rule that qualified an interval as a [`HangIncident`]. +/// +/// Recorded explicitly so consumers can separate the two classes without +/// re-deriving them from `stall_ms`, which stops working whenever the +/// detector's thresholds change. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum HangTrigger { + /// A single event blocked the foreground for at least the hang threshold. + Threshold, + /// No single event crossed the threshold, but the interval's total + /// foreground spend reached the frame budget. + Budget, +} + +impl HangDetector { + /// Creates a detector reporting single events at or above `threshold` + /// and intervals whose total foreground spend reached `frame_budget`. + /// Only events recorded from this point on are observed. + pub fn new(journal: ForegroundJournal, threshold: Duration, frame_budget: Duration) -> Self { + Self { + collector: journal.collector(), + sealer: IntervalSealer::new(Instant::now()), + threshold, + frame_budget, + first_present_at: None, + } + } + + /// When the first newly drawn frame observed by this detector finished + /// platform submission. `None` until a presentation boundary is observed. + pub fn first_present_at(&self) -> Option { + self.first_present_at + } + + /// Drains newly recorded events and returns the incidents sealed since + /// the previous poll. + pub fn poll(&mut self) -> Vec { + let drained = self.collector.collect_unseen(); + if self.first_present_at.is_none() { + self.first_present_at = drained.entries.iter().find_map(|entry| match entry { + ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) => { + Some(presented.presentation.present_end) + } + _ => None, + }); + } + self.sealer + .push_entries(drained.entries) + .into_iter() + .filter_map(|snapshot| { + HangIncident::detect(snapshot, self.threshold, self.frame_budget) + }) + .collect() + } +} + +/// A [`HangIncident`] in a telemetry-friendly form: timestamps and durations +/// in fractional milliseconds since app startup (microsecond precision), +/// locations as plain data, contributor count capped by the converter. +#[derive(Debug, Clone, Serialize)] +pub struct SerializedHangIncident { + /// Identifies the rules used to exclude interrupted measurements. + pub measurement_version: u32, + /// `"startup"` when the active window began before the first observed + /// newly drawn frame finished platform submission (see + /// [`HangDetector::first_present_at`]), otherwise `"steady"`. + pub phase: &'static str, + /// `"threshold"` or `"budget"` (see [`HangTrigger`]). + pub trigger: HangTrigger, + /// When the incident's active window started, in milliseconds since app + /// startup: the sealing frame's first invalidation, or the earliest + /// contributor's start when nothing was pending a repaint. Foreground + /// idle time between the previous frame and the cause is excluded. + pub start_ms: f64, + /// Length of the active window in milliseconds: from the cause to the + /// seal. Exceeds `stall_ms` when several stalls piled up on one frame. + pub active_ms: f64, + /// The longest single block of foreground work, in milliseconds: the + /// best estimate of the freeze a user perceived. Below the hang + /// threshold (possibly zero) when the frame budget alone triggered the + /// incident. + pub stall_ms: f64, + /// For presentation-sealed incidents, how long the submitted frame had + /// been dirty, in milliseconds. + pub dirty_to_present_ms: Option, + /// What closed the incident: `"present"`, `"frame_skipped"`, `"idle"`, or + /// `"power_transition"`. This labels the boundary, not the hang's cause — + /// the cause is the first contributor. + pub sealed_by: &'static str, + /// Fraction of the active window the foreground spent working, + /// `0.0..=1.0`. Low values with a high `dirty_to_present_ms` indicate + /// throttling or scheduling delay rather than application work. + pub busy_fraction: f64, + /// Total events recorded in the interval (before the contributor cap). + pub event_count: usize, + /// Count of task polls below the journal's floor. + pub small_poll_count: u64, + /// Total duration of task polls below the journal's floor, in milliseconds. + pub small_poll_total_ms: f64, + /// Events lost to caps or ring overwrites. + pub dropped_events: u64, + /// Whether one or more journal entries were unavailable in the interval. + /// Threshold-qualified contributors remain valid, but cumulative budget + /// conclusions and boundary attribution are not trusted across the gap. + pub journal_discontinuous: bool, + /// The incident's contributors (see [`HangIncident::contributors`]) in + /// start order. The cap keeps the longest ones; `stall_ms` is always + /// among them. + pub contributors: Vec, + /// Contributors elided by the cap. + pub contributors_elided: usize, +} + +/// One hang contributor in serialized form. +/// +/// Foreground work nests: an input dispatch can synchronously draw a window, +/// a draw can poll a task. `depth` expresses that containment — a depth-1 +/// event's time is already inside some depth-0 event's duration, so summing +/// sibling durations across depths double-counts. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum SerializedHangContributor { + /// A foreground task poll. + TaskPoll { + /// Where the task was spawned. + location: SerializedLocation, + /// When the poll started, in milliseconds since app startup. + start_ms: f64, + /// How long the poll blocked the foreground, in milliseconds. + duration_ms: f64, + /// How many other events in the interval contain this one. + depth: usize, + }, + /// An action handler. + Action { + /// The action's name. + name: &'static str, + /// When the handler started, in milliseconds since app startup. + start_ms: f64, + /// How long the handler ran, in milliseconds. + duration_ms: f64, + /// How many other events in the interval contain this one. + depth: usize, + }, + /// A platform input dispatch. + Input { + /// The platform input variant dispatched, e.g. `"key_down"`. + /// Named `input_kind` because `kind` is this enum's serde tag. + input_kind: &'static str, + /// When the dispatch started, in milliseconds since app startup. + start_ms: f64, + /// How long the dispatch ran, in milliseconds. + duration_ms: f64, + /// Whether handling the input invalidated a window. + caused_invalidation: bool, + /// How many other events in the interval contain this one. + depth: usize, + }, + /// A window draw. + Draw { + /// The window that was drawn. + window_id: u64, + /// When the draw started, in milliseconds since app startup. + start_ms: f64, + /// How long the draw took, in milliseconds. + duration_ms: f64, + /// Time from the frame's first invalidation to the end of its draw, + /// in milliseconds. + dirty_to_draw_ms: Option, + /// Invalidations coalesced into the frame. + invalidations: u64, + /// How many other events in the interval contain this one. + depth: usize, + }, + /// Work spent submitting a frame to the platform. + Present { + /// The window whose frame was submitted. + window_id: u64, + /// When submission began, in milliseconds since app startup. + start_ms: f64, + /// How long platform submission took, in milliseconds. + duration_ms: f64, + /// How many other events in the interval contain this one. + depth: usize, + }, +} + +/// Milliseconds with microsecond precision: keeps `dbg!`/JSON output short +/// (`115.954` rather than `115.95400000000001` or `115954`). +fn as_millis(duration: Duration) -> f64 { + duration.as_micros() as f64 / 1000.0 +} + +impl SerializedHangIncident { + /// Converts an incident, keeping at most `max_contributors` contributors. + /// `first_present_at` is the end of the first observed newly drawn frame's + /// platform submission (typically [`HangDetector::first_present_at`]); + /// incidents whose active window begins before it are tagged `"startup"`. + pub fn convert( + startup: Instant, + incident: &HangIncident, + max_contributors: usize, + first_present_at: Option, + ) -> Self { + let since_startup = + |instant: Instant| as_millis(instant.saturating_duration_since(startup)); + let snapshot = &incident.snapshot; + let (active_start, active_end) = incident.active_window(); + let active = active_end.duration_since(active_start); + let busy_fraction = if active.is_zero() { + 1.0 + } else { + snapshot + .occupancy_within(active_start, active_end) + .div_duration_f64(active) + .min(1.0) + }; + Self { + phase: match first_present_at { + Some(first_present_at) if active_start >= first_present_at => "steady", + _ => "startup", + }, + measurement_version: MEASUREMENT_VERSION, + trigger: incident.trigger, + start_ms: since_startup(active_start), + active_ms: as_millis(active), + stall_ms: incident + .contributors + .first() + .map(|event| as_millis(event.duration())) + .unwrap_or(0.0), + dirty_to_present_ms: match snapshot.boundary { + IntervalBoundary::Presented(presented) => { + presented.dirty_to_present_duration().map(as_millis) + } + IntervalBoundary::FrameSkipped(_) + | IntervalBoundary::Idle { .. } + | IntervalBoundary::PowerTransition { .. } => None, + }, + sealed_by: match snapshot.boundary { + IntervalBoundary::Presented(_) => "present", + IntervalBoundary::FrameSkipped(_) => "frame_skipped", + IntervalBoundary::Idle { .. } => "idle", + IntervalBoundary::PowerTransition { .. } => "power_transition", + }, + busy_fraction: (busy_fraction * 1000.0).round() / 1000.0, + event_count: snapshot.events.len(), + small_poll_count: snapshot.small_poll_summary().count, + small_poll_total_ms: as_millis(snapshot.small_poll_summary().total), + dropped_events: snapshot.dropped_events, + journal_discontinuous: snapshot.journal_discontinuous, + contributors: { + let mut kept: Vec<&ForegroundEvent> = incident + .contributors + .iter() + .take(max_contributors) + .collect(); + kept.sort_by_key(|event| event.start_time()); + kept.into_iter() + .map(|event| { + SerializedHangContributor::convert( + startup, + event, + nesting_depth(event, &snapshot.events), + ) + }) + .collect() + }, + contributors_elided: incident.contributors.len().saturating_sub(max_contributors), + } + } +} + +/// How many events in `events` strictly contain `event`'s span. Events with +/// identical spans don't count as containing each other, so `event`'s own +/// presence in `events` contributes nothing. +fn nesting_depth(event: &ForegroundEvent, events: &[ForegroundEvent]) -> usize { + let (start, end) = (event.start_time(), event.end_time()); + events + .iter() + .filter(|other| { + let (other_start, other_end) = (other.start_time(), other.end_time()); + other_start <= start && end <= other_end && (other_start < start || end < other_end) + }) + .count() +} + +impl SerializedHangContributor { + fn convert(startup: Instant, event: &ForegroundEvent, depth: usize) -> Self { + let since_startup = + |instant: Instant| as_millis(instant.saturating_duration_since(startup)); + let duration_ms = as_millis(event.duration()); + match event { + ForegroundEvent::TaskPoll(timing) => Self::TaskPoll { + location: timing.location.into(), + start_ms: since_startup(timing.start), + duration_ms, + depth, + }, + ForegroundEvent::Action(timing) => Self::Action { + name: timing.name, + start_ms: since_startup(timing.start), + duration_ms, + depth, + }, + ForegroundEvent::Input(timing) => Self::Input { + input_kind: timing.kind, + start_ms: since_startup(timing.start), + duration_ms, + caused_invalidation: timing.caused_invalidation, + depth, + }, + ForegroundEvent::Draw(timing) => Self::Draw { + window_id: timing.window_id.as_u64(), + start_ms: since_startup(timing.draw_start), + duration_ms, + dirty_to_draw_ms: timing.dirty_to_draw_duration().map(as_millis), + invalidations: timing.invalidations, + depth, + }, + ForegroundEvent::Present(timing) => Self::Present { + window_id: timing.window_id.as_u64(), + start_ms: since_startup(timing.present_start), + duration_ms, + depth, + }, + ForegroundEvent::SmallPolls(flush) => { + // The sealer folds these out of snapshot events; a contributor + // can therefore never be one, but serialize defensively as an + // unnamed poll spanning the flush rather than panicking. + Self::TaskPoll { + location: SerializedLocation { + file: "".into(), + line: 0, + column: 0, + }, + start_ms: since_startup(flush.since), + duration_ms, + depth, + } + } + } + } +} + +impl HangIncident { + /// The incident's reporting window: from its earliest cause — the + /// sealing frame's first invalidation, or the earliest contributor's + /// start when no repaint was pending — to the seal. This trims + /// foreground-idle time between the previous frame and the cause, and + /// may begin before the underlying snapshot when a contributor was + /// already running at the previous seal. + pub fn active_window(&self) -> (Instant, Instant) { + let snapshot = &self.snapshot; + let dirty_at = snapshot.boundary.dirty_at(); + let earliest_contributor = self + .contributors + .iter() + .map(|event| event.start_time()) + .min(); + let start = [dirty_at, earliest_contributor] + .into_iter() + .flatten() + .min() + .unwrap_or(snapshot.interval_start); + (start, snapshot.interval_end()) + } + + /// Returns an incident when the snapshot contains at least one event + /// that blocked the foreground for `threshold` or longer, or when the + /// interval's total foreground spend — event time plus folded + /// small-poll time — reached `frame_budget` even though no single event + /// did. In the latter case every event in the interval becomes a + /// contributor, since no single stall explains the busy interval. + pub fn detect( + snapshot: FrameSnapshot, + threshold: Duration, + frame_budget: Duration, + ) -> Option { + let mut contributors: Vec = snapshot + .events + .iter() + .filter(|event| event.duration() >= threshold) + .copied() + .collect(); + let trigger = if contributors.is_empty() { + if snapshot.journal_discontinuous { + return None; + } + let spend = snapshot.occupancy_within(snapshot.interval_start, snapshot.interval_end()); + if spend < frame_budget { + return None; + } + contributors = snapshot.events.clone(); + HangTrigger::Budget + } else { + HangTrigger::Threshold + }; + contributors.sort_by_key(|event| std::cmp::Reverse(event.duration())); + Some(Self { + snapshot, + trigger, + contributors, + }) + } +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::rc::Rc; + use std::thread; + use std::time::Duration; + + use proptest::prelude::*; + use rand::prelude::*; + use scheduler::SpawnTime; + + use crate::{ + self as gpui, Context, FocusHandle, InteractiveElement, IntoElement, Modifiers, + MouseButton, Render, Styled, TestAppContext, VisualTestContext, Window, WindowId, div, + point, px, + }; + + use crate::profiler::{ActionTiming, FrameTiming, PresentTiming, TaskTiming, YieldTime}; + + use super::super::journal::{ + FRAME_DEADLINE, ForegroundEvent, ForegroundJournalEntry, FrameSkipReason, FrameSkipped, + FrameSnapshot, FrameStateChange, InputTiming, IntervalBoundary, IntervalSealer, + PlatformSignal, PollSummary, PresentedFrame, SmallPollFlush, + install_test_foreground_journal, record_present, + }; + use super::{ + HangDetector, HangIncident, HangTrigger, SerializedHangContributor, SerializedHangIncident, + }; + + actions!(hang_test, [HangyAction]); + + // Well above legitimate per-event work in a test app (layout of one div, + // empty polls), well below the injected hangs. + const HANG_THRESHOLD: Duration = Duration::from_millis(10); + // Equal to the threshold, mirroring how production wires the detector + // today. + const FRAME_BUDGET: Duration = HANG_THRESHOLD; + + #[test] + fn completed_skips_preserve_real_hangs_and_serialize_their_boundary() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(1); + for discontinuous in [false, true] { + for trigger in [HangTrigger::Threshold, HangTrigger::Budget] { + let mut sealer = IntervalSealer::new(startup); + let mut entries = Vec::new(); + match trigger { + HangTrigger::Threshold => entries.push(ForegroundJournalEntry::Event( + task_poll_event(at(10), at(30)), + )), + HangTrigger::Budget => entries.extend([ + ForegroundJournalEntry::Event(task_poll_event(at(10), at(16))), + ForegroundJournalEntry::Event(task_poll_event(at(20), at(26))), + ]), + } + if discontinuous { + entries.push(ForegroundJournalEntry::Discontinuity { lost: 1 }); + } + entries.push(ForegroundJournalEntry::Boundary( + IntervalBoundary::FrameSkipped(FrameSkipped { + window_id, + at: at(60), + reason: FrameSkipReason::NoRenderNeeded, + }), + )); + let snapshots = sealer.push_entries(entries); + let [snapshot] = snapshots.as_slice() else { + panic!("expected one snapshot, got {snapshots:?}"); + }; + let incident = HangIncident::detect(snapshot.clone(), HANG_THRESHOLD, FRAME_BUDGET); + if discontinuous && trigger == HangTrigger::Budget { + assert!(incident.is_none(), "skips must not exempt journal gaps"); + continue; + } + let incident = incident.expect("real work still qualifies across a skip"); + assert_eq!(incident.trigger, trigger); + let serialized = SerializedHangIncident::convert(startup, &incident, 10, None); + assert_eq!(serialized.sealed_by, "frame_skipped"); + assert_eq!(serialized.dirty_to_present_ms, None); + assert_eq!(serialized.start_ms, 10.0); + assert_eq!(serialized.active_ms, 50.0); + assert_eq!(serialized.journal_discontinuous, discontinuous); + assert_eq!(serialized.event_count, incident.contributors.len()); + let json = serde_json::to_value(&serialized).expect("serialize incident"); + assert_eq!(json["sealed_by"], "frame_skipped"); + assert!(json["dirty_to_present_ms"].is_null()); + } + } + } + + #[test] + fn delayed_platform_signals_without_work_are_not_hangs() { + let start = scheduler::Instant::now(); + let window_id = WindowId::from(1); + let mut sealer = IntervalSealer::new(start); + let snapshots = sealer.push_entries([ + ForegroundJournalEntry::PlatformSignal(PlatformSignal { + window_id, + signal_at: Some(start), + handled_at: start + Duration::from_secs(5), + source: crate::FrameRequestSource::NativeCallback, + }), + ForegroundJournalEntry::Boundary(IntervalBoundary::FrameSkipped(FrameSkipped { + window_id, + at: start + Duration::from_secs(6), + reason: FrameSkipReason::NoRenderNeeded, + })), + ]); + assert!( + snapshots.is_empty(), + "a late signal without work seals nothing, got {snapshots:?}" + ); + } + + /// A hang that outlives the frame deadline stays in one incident with + /// the frame it starved: the deadline only unblocks idle boundaries, so + /// the eventual presentation seals the hang together with its + /// dirty-to-present association. + #[test] + fn a_hang_outliving_the_frame_deadline_keeps_its_frame_association() { + let start = scheduler::Instant::now(); + let window_id = WindowId::from(0x51E17); + let hang_end = start + FRAME_DEADLINE * 5; + let presented_at = hang_end + Duration::from_millis(16); + let (journal, _journal_guard) = install_test_foreground_journal(64, 8); + let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); + + let snapshots = detector.sealer.push_entries([ + ForegroundJournalEntry::FrameState(FrameStateChange::Pending { + window_id, + dirty_at: start, + }), + ForegroundJournalEntry::Event(task_poll_event(start, hang_end)), + ]); + assert!(snapshots.is_empty(), "nothing seals mid-hang"); + + let snapshots = detector + .sealer + .push_entries([ForegroundJournalEntry::Boundary( + IntervalBoundary::Presented(PresentedFrame { + frame: frame(window_id, presented_at), + presentation: PresentTiming { + window_id, + present_start: presented_at - Duration::from_millis(1), + present_end: presented_at, + animation_interval: None, + }, + }), + )]); + let [snapshot] = snapshots.as_slice() else { + panic!("expected one presented snapshot, got {snapshots:?}"); + }; + let incident = HangIncident::detect(snapshot.clone(), HANG_THRESHOLD, FRAME_BUDGET) + .expect("the hang qualifies"); + assert!(matches!( + incident.contributors[0], + ForegroundEvent::TaskPoll(timing) if timing.end.0 == hang_end + )); + assert!(matches!( + incident.snapshot.boundary, + IntervalBoundary::Presented(_) + )); + } + + #[test] + fn serialized_incident_reports_presented_seal_fields() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(0xF1E1D); + + let presentation = PresentTiming { + window_id, + present_start: at(380), + present_end: at(400), + animation_interval: None, + }; + let frame = FrameTiming { + window_id, + dirty_at: Some(at(100)), + invalidations: 3, + draw_start: at(350), + draw_end: at(380), + }; + let snapshot = FrameSnapshot { + interval_start: at(150), + boundary: IntervalBoundary::Presented(PresentedFrame { + frame, + presentation, + }), + events: vec![ + task_poll_event(at(150), at(300)), + ForegroundEvent::Action(ActionTiming { + name: "test::SlowAction", + start: at(300), + end: at(340), + }), + ForegroundEvent::Input(InputTiming { + kind: "test", + start: at(340), + end: at(345), + caused_invalidation: false, + }), + ForegroundEvent::Present(presentation), + ], + small_polls: vec![SmallPollFlush { + summary: PollSummary { + count: 2, + total: Duration::from_millis(1), + }, + since: at(105), + until: at(145), + }], + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = + HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); + let serialized = SerializedHangIncident::convert(startup, &incident, 1, Some(at(50))); + + assert_eq!(serialized.phase, "steady"); + assert_eq!(serialized.trigger, HangTrigger::Threshold); + // The frame's first invalidation anchors the active window, not the + // interval start or the first contributor. + assert_eq!(serialized.start_ms, 100.0); + assert_eq!(serialized.active_ms, 300.0); + assert_eq!(serialized.stall_ms, 150.0); + assert_eq!(serialized.dirty_to_present_ms, Some(300.0)); + assert_eq!(serialized.sealed_by, "present"); + // Occupancy: poll 150ms + action 40ms + input 5ms + present 20ms + + // folded polls 1ms = 216ms of the 300ms window. + assert_eq!(serialized.busy_fraction, 0.72); + assert_eq!(serialized.event_count, 4); + assert_eq!(serialized.small_poll_count, 2); + assert_eq!(serialized.small_poll_total_ms, 1.0); + assert_eq!(serialized.dropped_events, 0); + // Three contributors qualify (poll, action, present); the cap keeps + // the longest and counts the rest. + assert_eq!(serialized.contributors.len(), 1); + assert_eq!(serialized.contributors_elided, 2); + assert!(matches!( + serialized.contributors[0], + SerializedHangContributor::TaskPoll { + start_ms, + duration_ms, + .. + } if start_ms == 150.0 && duration_ms == 150.0 + )); + } + + #[test] + fn hang_monitor_polls_on_its_interval_and_on_flush() { + use super::{HangMonitor, HangMonitorPollReason}; + + let (journal, _guard) = install_test_foreground_journal(64, 4); + // Each monitor reports on its own channel, which disconnects once the + // monitor's thread has exited and dropped its callback. + let spawn_monitor = |journal, interval| { + let (sender, receiver) = std::sync::mpsc::channel(); + let monitor = HangMonitor::spawn( + HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET), + interval, + move |poll| { + if !poll.incidents.is_empty() { + sender.send((poll.reason, poll.incidents.len())).ok(); + } + }, + ) + .expect("spawn monitor thread"); + (monitor, receiver) + }; + + let (monitor, receiver) = spawn_monitor(journal.clone(), Duration::from_millis(10)); + simulate_blocked_foreground_poll(HANG_THRESHOLD * 2); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(10)).ok(), + Some((HangMonitorPollReason::Interval, 1)), + ); + drop(monitor); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(10)), + Err(std::sync::mpsc::RecvTimeoutError::Disconnected), + ); + + // An interval longer than the test isolates the flush path. + let (monitor, receiver) = spawn_monitor(journal, Duration::from_secs(3600)); + simulate_blocked_foreground_poll(HANG_THRESHOLD * 2); + let flushed = monitor.request_flush().expect("monitor thread is running"); + futures::executor::block_on(flushed).expect("flush completes"); + assert_eq!( + receiver.try_recv().ok(), + Some((HangMonitorPollReason::Flush, 1)), + ); + + drop(monitor); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(10)), + Err(std::sync::mpsc::RecvTimeoutError::Disconnected), + ); + } + + #[gpui::test] + fn app_flushes_its_hang_monitor_on_shutdown(cx: &mut TestAppContext) { + use super::{HangMonitorConfig, HangMonitorPollReason}; + + // The monitor runs on a real OS thread, so shutdown has to park until it + // finishes the flush, and its completion wakes the scheduler from that thread. + cx.executor().allow_parking(); + + let (sender, receiver) = std::sync::mpsc::channel(); + cx.update(|cx| { + cx.start_hang_monitor( + HangMonitorConfig { + threshold: HANG_THRESHOLD, + frame_budget: FRAME_BUDGET, + // Longer than the test, so only shutdown can poll. + interval: Duration::from_secs(3600), + }, + move |poll| { + sender.send(poll.reason).ok(); + }, + ) + .expect("start hang monitor"); + }); + assert!(receiver.try_recv().is_err()); + + cx.update(|cx| cx.shutdown()); + assert_eq!(receiver.try_recv().ok(), Some(HangMonitorPollReason::Flush)); + } + + #[gpui::test] + fn starting_the_hang_monitor_twice_is_rejected(cx: &mut TestAppContext) { + use super::{HangMonitorConfig, HangMonitorError}; + + let config = HangMonitorConfig { + threshold: HANG_THRESHOLD, + frame_budget: FRAME_BUDGET, + interval: Duration::from_secs(3600), + }; + cx.update(|cx| { + cx.start_hang_monitor(config, |_| {}) + .expect("start hang monitor"); + let second = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + cx.start_hang_monitor(config, |_| {}) + })); + // Debug builds also assert, so misuse is caught during development. + if cfg!(debug_assertions) { + assert!(second.is_err()); + } else { + assert!(matches!(second, Ok(Err(HangMonitorError::AlreadyStarted)))); + } + }); + } + + #[test] + fn serialized_incident_reports_idle_seal_fields() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + + let idle = FrameSnapshot { + interval_start: at(200), + boundary: IntervalBoundary::Idle { ended_at: at(260) }, + events: vec![task_poll_event(at(200), at(260))], + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + let incident = + HangIncident::detect(idle, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, None); + assert_eq!(serialized.phase, "startup"); + assert_eq!(serialized.sealed_by, "idle"); + assert_eq!(serialized.dirty_to_present_ms, None); + // With no frame, the earliest contributor anchors the active window. + assert_eq!(serialized.start_ms, 200.0); + assert_eq!(serialized.active_ms, 60.0); + assert_eq!(serialized.stall_ms, 60.0); + assert_eq!(serialized.busy_fraction, 1.0); + } + + #[test] + fn phase_is_startup_until_the_first_present() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let snapshot = FrameSnapshot { + interval_start: at(500), + boundary: IntervalBoundary::Idle { ended_at: at(600) }, + events: vec![task_poll_event(at(500), at(600))], + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + let incident = + HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); + + let phase = |first_present_at| { + SerializedHangIncident::convert(startup, &incident, 8, first_present_at).phase + }; + assert_eq!(phase(None), "startup", "nothing has been presented yet"); + assert_eq!( + phase(Some(at(700))), + "startup", + "the incident began before the first presentation" + ); + assert_eq!( + phase(Some(at(500))), + "steady", + "an incident beginning exactly at first presentation is steady" + ); + assert_eq!(phase(Some(at(100))), "steady"); + } + + /// The detector must latch the first presentation it observes and never + /// move it. + #[test] + fn first_present_at_latches_on_the_first_observed_presentation() { + let (journal, _journal_guard) = install_test_foreground_journal(64, 8); + let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); + let window_id = WindowId::from(0x1A7C4); + + let first_present_end = scheduler::Instant::now(); + record_present( + presentation(window_id, first_present_end), + Some(frame(window_id, first_present_end)), + ); + detector.poll(); + let latched = detector + .first_present_at() + .expect("a presentation was recorded"); + assert_eq!(latched, first_present_end); + + record_present( + presentation(window_id, first_present_end + Duration::from_millis(16)), + Some(frame( + window_id, + first_present_end + Duration::from_millis(16), + )), + ); + detector.poll(); + assert_eq!(detector.first_present_at(), Some(latched)); + } + + /// The only work inside the 100ms active window here is the 50ms poll, + /// so the busy fraction is 0.5: the folded polls' flush span lies + /// entirely before the window and must contribute nothing to it + /// (PR #62779 review finding 5). + #[test] + fn busy_fraction_excludes_small_polls_outside_the_active_window() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Idle { ended_at: at(1000) }, + events: vec![task_poll_event(at(900), at(950))], + // Folded polls that ran during at(0)..at(800), long before the + // active window opens at the contributor's start. + small_polls: vec![SmallPollFlush { + summary: PollSummary { + count: 500, + total: Duration::from_millis(400), + }, + since: at(0), + until: at(800), + }], + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = + HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, None); + + assert_eq!(serialized.busy_fraction, 0.5); + } + + /// Many sub-threshold pieces of work can drop a frame as thoroughly as + /// one long stall: an interval whose total foreground spend reaches the + /// budget is an incident even when no single event crosses the hang + /// threshold, and every event becomes a contributor so the report shows + /// what filled the interval. + #[test] + fn an_interval_of_small_work_over_budget_is_an_incident() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(0xB0D6E7); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Presented(PresentedFrame { + frame: FrameTiming { + window_id, + dirty_at: Some(at(0)), + invalidations: 1, + draw_start: at(140), + draw_end: at(145), + }, + presentation: PresentTiming { + window_id, + present_start: at(145), + present_end: at(150), + animation_interval: None, + }, + }), + events: vec![ + task_poll_event(at(0), at(5)), + task_poll_event(at(5), at(13)), + task_poll_event(at(13), at(18)), + ], + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) + .expect("foreground spend exceeded the frame budget"); + assert_eq!(incident.trigger, HangTrigger::Budget); + assert_eq!(incident.contributors.len(), 3); + assert_eq!( + incident.contributors[0].duration(), + Duration::from_millis(8) + ); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); + assert_eq!(serialized.trigger, HangTrigger::Budget); + assert_eq!(serialized.stall_ms, 8.0); + assert_eq!(serialized.dirty_to_present_ms, Some(150.0)); + assert_eq!(serialized.sealed_by, "present"); + } + + #[test] + fn a_journal_gap_suppresses_budget_inference_but_retains_observed_hangs() { + let start = scheduler::Instant::now(); + let at = |ms: u64| start + Duration::from_millis(ms); + let mut sealer = super::super::journal::IntervalSealer::new(start); + let snapshots = sealer.push_entries([ + ForegroundJournalEntry::Event(task_poll_event(at(0), at(5))), + ForegroundJournalEntry::Discontinuity { lost: 1 }, + ForegroundJournalEntry::Event(task_poll_event(at(10), at(15))), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(15) }), + ForegroundJournalEntry::Discontinuity { lost: 1 }, + ForegroundJournalEntry::Event(task_poll_event(at(20), at(32))), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(32) }), + ForegroundJournalEntry::Event(task_poll_event(at(40), at(52))), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: at(52) }), + ]); + let [budget_only, observed_hang, clean] = snapshots.as_slice() else { + panic!("expected two discontinuous snapshots followed by a clean one"); + }; + + assert!(budget_only.journal_discontinuous); + assert_eq!(budget_only.dropped_events, 1); + assert!(HangIncident::detect(budget_only.clone(), HANG_THRESHOLD, FRAME_BUDGET).is_none()); + + let observed_hang = + HangIncident::detect(observed_hang.clone(), HANG_THRESHOLD, FRAME_BUDGET) + .expect("the directly observed threshold-qualified hang remains valid"); + assert!(observed_hang.snapshot.journal_discontinuous); + assert_eq!(observed_hang.contributors.len(), 1); + let serialized = SerializedHangIncident::convert(start, &observed_hang, 8, None); + assert!(serialized.journal_discontinuous); + + assert!(!clean.journal_discontinuous); + assert!(HangIncident::detect(clean.clone(), HANG_THRESHOLD, FRAME_BUDGET).is_some()); + } + + /// A frame that reaches the screen late with almost no foreground work + /// behind it — pure scheduling or presentation delay — is not a hang: + /// the budget measures foreground spend, not dirty-to-present time. + #[test] + fn a_slow_frame_with_little_foreground_spend_is_not_an_incident() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(0xFA57); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Presented(PresentedFrame { + frame: FrameTiming { + window_id, + dirty_at: Some(at(0)), + invalidations: 1, + draw_start: at(145), + draw_end: at(147), + }, + presentation: PresentTiming { + window_id, + present_start: at(149), + present_end: at(150), + animation_interval: None, + }, + }), + events: vec![task_poll_event(at(0), at(5))], + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + + assert!(HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).is_none()); + } + + /// The budget applies to idle-sealed intervals too: a headless app (or + /// a stretch with no repaint pending) can still starve the foreground + /// with accumulated sub-threshold work. + #[test] + fn an_idle_sealed_interval_over_budget_is_an_incident() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Idle { ended_at: at(200) }, + events: (0..10) + .map(|i| task_poll_event(at(i * 20), at(i * 20 + 5))) + .collect(), + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) + .expect("foreground spend exceeded the frame budget"); + assert_eq!(incident.contributors.len(), 10); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); + assert_eq!(serialized.sealed_by, "idle"); + assert_eq!(serialized.dirty_to_present_ms, None); + assert_eq!(serialized.stall_ms, 5.0); + assert_eq!(serialized.contributors_elided, 2); + } + + /// Contributors serialize in start order with their nesting depth: an + /// input dispatch that synchronously drew a window reads as the input at + /// depth 0 followed by the draw at depth 1, rather than two unrelated + /// blocks of equal wall time. + #[test] + fn serialized_contributors_are_chronological_with_nesting_depths() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(0x2E57ED); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Idle { ended_at: at(80) }, + events: vec![ + task_poll_event(at(50), at(70)), + ForegroundEvent::Input(InputTiming { + kind: "mouse_move", + start: at(0), + end: at(40), + caused_invalidation: true, + }), + ForegroundEvent::Draw(FrameTiming { + window_id, + dirty_at: Some(at(0)), + invalidations: 1, + draw_start: at(1), + draw_end: at(39), + }), + ], + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = + HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET).expect("has contributors"); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); + + assert_eq!(serialized.stall_ms, 40.0); + assert!(matches!( + serialized.contributors.as_slice(), + [ + SerializedHangContributor::Input { + input_kind: "mouse_move", + depth: 0, + .. + }, + SerializedHangContributor::Draw { depth: 1, .. }, + SerializedHangContributor::TaskPoll { depth: 0, .. }, + ] + )); + } + + /// Folded small polls count toward foreground spend, so an interval can + /// reach the budget with no retained events at all. The incident then + /// has no contributors and the small-poll summary carries the story. + #[test] + fn small_poll_spend_alone_can_reach_the_budget() { + let startup = scheduler::Instant::now(); + let at = |ms: u64| startup + Duration::from_millis(ms); + let window_id = WindowId::from(0xDE1A7); + let snapshot = FrameSnapshot { + interval_start: at(0), + boundary: IntervalBoundary::Presented(PresentedFrame { + frame: FrameTiming { + window_id, + dirty_at: Some(at(0)), + invalidations: 1, + draw_start: at(148), + draw_end: at(149), + }, + presentation: PresentTiming { + window_id, + present_start: at(149), + present_end: at(150), + animation_interval: None, + }, + }), + events: Vec::new(), + small_polls: vec![SmallPollFlush { + summary: PollSummary { + count: 40, + total: Duration::from_millis(12), + }, + since: at(0), + until: at(150), + }], + dropped_events: 0, + journal_discontinuous: false, + }; + + let incident = HangIncident::detect(snapshot, HANG_THRESHOLD, FRAME_BUDGET) + .expect("small-poll spend exceeded the frame budget"); + assert!(incident.contributors.is_empty()); + let serialized = SerializedHangIncident::convert(startup, &incident, 8, Some(startup)); + assert_eq!(serialized.stall_ms, 0.0); + assert_eq!(serialized.event_count, 0); + assert_eq!(serialized.small_poll_count, 40); + assert_eq!(serialized.dirty_to_present_ms, Some(150.0)); + assert_eq!(serialized.busy_fraction, 0.08); + } + + proptest! { + #![proptest_config(ProptestConfig { + failure_persistence: None, + ..ProptestConfig::default() + })] + + #[test] + fn detection_matches_threshold_and_budget_model_at_boundaries( + durations_ms in prop::collection::vec(0u16..=50, 0..=16), + threshold_ms in 0u16..=60, + frame_budget_ms in 0u16..=500, + small_poll_total_ms in 0u16..=100, + ) { + let origin = scheduler::Instant::now(); + let mut cursor_ms = 0u64; + let events = durations_ms + .iter() + .map(|duration_ms| { + let start = origin + Duration::from_millis(cursor_ms); + cursor_ms += u64::from(*duration_ms); + let end = origin + Duration::from_millis(cursor_ms); + cursor_ms += 1; + ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end, + caused_invalidation: false, + }) + }) + .collect::>(); + let interval_end_ms = cursor_ms.max(1); + let snapshot = FrameSnapshot { + interval_start: origin, + boundary: IntervalBoundary::Idle { + ended_at: origin + Duration::from_millis(interval_end_ms), + }, + events, + small_polls: vec![SmallPollFlush { + summary: PollSummary { + count: u64::from(small_poll_total_ms > 0), + total: Duration::from_millis(u64::from(small_poll_total_ms)), + }, + since: origin, + until: origin + Duration::from_millis(interval_end_ms), + }], + dropped_events: 0, + journal_discontinuous: false, + }; + let qualifying_durations = durations_ms + .iter() + .copied() + .filter(|duration| *duration >= threshold_ms) + .collect::>(); + let occupancy_ms = durations_ms + .iter() + .map(|duration| u64::from(*duration)) + .sum::() + + u64::from(small_poll_total_ms); + let expected_incident = + !qualifying_durations.is_empty() || occupancy_ms >= u64::from(frame_budget_ms); + + let incident = HangIncident::detect( + snapshot, + Duration::from_millis(u64::from(threshold_ms)), + Duration::from_millis(u64::from(frame_budget_ms)), + ); + prop_assert_eq!(incident.is_some(), expected_incident); + + if let Some(incident) = incident { + let mut expected_contributors = if qualifying_durations.is_empty() { + durations_ms + } else { + qualifying_durations + }; + expected_contributors.sort_unstable_by(|first, second| second.cmp(first)); + let observed_contributors = incident + .contributors + .iter() + .map(|event| event.duration().as_millis() as u16) + .collect::>(); + prop_assert_eq!(observed_contributors, expected_contributors); + } + } + } + + #[derive(Clone, Default)] + struct HangControls { + render: Rc>>, + input: Rc>>, + action: Rc>>, + } + + struct HangyView { + controls: HangControls, + focus_handle: FocusHandle, + } + + impl Render for HangyView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + if let Some(duration) = self.controls.render.take() { + thread::sleep(duration); + } + let action_controls = self.controls.clone(); + let input_controls = self.controls.clone(); + div() + .size_full() + .track_focus(&self.focus_handle) + .on_action(cx.listener(move |_, _: &HangyAction, _, _| { + if let Some(duration) = action_controls.action.take() { + thread::sleep(duration); + } + })) + .on_mouse_down(MouseButton::Left, move |_, _, _| { + if let Some(duration) = input_controls.input.take() { + thread::sleep(duration); + } + }) + } + } + + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + enum HangKind { + Render, + Input, + Action, + Poll, + } + + /// Renders a real element tree and injects randomly ordered, randomly + /// sized hangs through the production dispatch paths: view rendering + /// (window draw), platform input dispatch, action dispatch, and a + /// foreground task poll. Asserts the detector reports every one of them. + #[gpui::test(iterations = 5)] + fn detects_randomly_placed_foreground_hangs(mut rng: StdRng, cx: &mut TestAppContext) { + let (journal, _journal_guard) = install_test_foreground_journal(1024, 16); + let mut detector = HangDetector::new(journal, HANG_THRESHOLD, FRAME_BUDGET); + + let controls = HangControls::default(); + let (view, cx) = cx.add_window_view(|_, cx| HangyView { + controls: controls.clone(), + focus_handle: cx.focus_handle(), + }); + view.update_in(cx, |view, window, cx| { + window.focus(&view.focus_handle, cx); + }); + draw_window(cx); + + let mut injections: Vec = Vec::new(); + for _ in 0..rng.random_range(1..=4) { + let duration = HANG_THRESHOLD + Duration::from_millis(rng.random_range(5..25)); + let started = scheduler::Instant::now(); + let kind = match rng.random_range(0..4) { + 0 => { + controls.render.set(Some(duration)); + view.update_in(cx, |_, _, cx| cx.notify()); + draw_window(cx); + HangKind::Render + } + 1 => { + controls.input.set(Some(duration)); + cx.simulate_mouse_down( + point(px(5.), px(5.)), + MouseButton::Left, + Modifiers::none(), + ); + HangKind::Input + } + 2 => { + controls.action.set(Some(duration)); + cx.dispatch_action(HangyAction); + HangKind::Action + } + _ => { + simulate_blocked_foreground_poll(duration); + HangKind::Poll + } + }; + injections.push(Injection { + kind, + duration, + window: started..scheduler::Instant::now(), + }); + + // Innocent interleaved activity that must not confuse detection. + if rng.random_bool(0.5) { + draw_window(cx); + } + if rng.random_bool(0.5) { + cx.simulate_mouse_up(point(px(5.), px(5.)), MouseButton::Left, Modifiers::none()); + } + } + + // A final presentation seals whatever interval is still open. + draw_window(cx); + + let incidents = detector.poll(); + let contributors: Vec = incidents + .iter() + .flat_map(|incident| incident.contributors.iter().copied()) + .collect(); + + for injection in &injections { + assert_detected_once(injection, &contributors); + } + } + + struct Injection { + kind: HangKind, + duration: Duration, + window: std::ops::Range, + } + + /// Exactly one contributor of the injected kind must fall inside the + /// injection's window, and it must be at least as long as the injected + /// sleep (sleeps never wake early). Contributors outside every window are + /// ignored: on a loaded machine, uninjected work can legitimately exceed + /// the threshold, and reporting it is correct. + fn assert_detected_once(injection: &Injection, contributors: &[ForegroundEvent]) { + let Injection { + kind, + duration, + window, + } = injection; + let matches: Vec<&ForegroundEvent> = contributors + .iter() + .filter(|event| { + matches_kind(event, *kind) + && window.start <= event.start_time() + && event.end_time() <= window.end + }) + .collect(); + assert_eq!( + matches.len(), + 1, + "expected injected {kind:?} hang of {duration:?} to be reported exactly once; \ + observed {:?}", + matches + .iter() + .map(|event| event.duration()) + .collect::>(), + ); + if let Some(event) = matches.first() { + assert!( + event.duration() >= *duration, + "injected {kind:?} hang of {duration:?} was reported as {:?}", + event.duration(), + ); + } + } + + fn matches_kind(event: &ForegroundEvent, kind: HangKind) -> bool { + match (event, kind) { + (ForegroundEvent::Draw(_), HangKind::Render) => true, + (ForegroundEvent::Input(_), HangKind::Input) => true, + (ForegroundEvent::Action(timing), HangKind::Action) => { + timing.name.ends_with("HangyAction") + } + (ForegroundEvent::TaskPoll(timing), HangKind::Poll) => { + timing.location.file() == file!() + } + _ => false, + } + } + + fn draw_window(cx: &mut VisualTestContext) { + cx.update(|window, cx| { + let arena_clear = window.draw(cx); + window.present_if_needed(); + arena_clear.clear(cx); + }); + } + + /// The deterministic test scheduler does not bracket runnables with the + /// profiler hooks, so drive the same public hooks the platform + /// dispatchers call around a poll that blocks the foreground. + fn simulate_blocked_foreground_poll(duration: Duration) { + let location = std::panic::Location::caller(); + crate::profiler::update_running_task(SpawnTime(scheduler::Instant::now()), location); + thread::sleep(duration); + crate::profiler::save_task_timing(); + } + + fn task_poll_event(start: scheduler::Instant, end: scheduler::Instant) -> ForegroundEvent { + ForegroundEvent::TaskPoll(TaskTiming { + location: std::panic::Location::caller(), + spawned: SpawnTime(start), + start, + end: YieldTime(end), + }) + } + + fn presentation(window_id: WindowId, present_end: scheduler::Instant) -> PresentTiming { + PresentTiming { + window_id, + present_start: present_end - Duration::from_millis(1), + present_end, + animation_interval: None, + } + } + + fn frame(window_id: WindowId, draw_end: scheduler::Instant) -> FrameTiming { + FrameTiming { + window_id, + dirty_at: Some(draw_end - Duration::from_millis(2)), + invalidations: 1, + draw_start: draw_end - Duration::from_millis(1), + draw_end, + } + } +} diff --git a/vendor/gpui-pre/src/profiler/hang/monitor.rs b/vendor/gpui-pre/src/profiler/hang/monitor.rs new file mode 100644 index 0000000..566b32d --- /dev/null +++ b/vendor/gpui-pre/src/profiler/hang/monitor.rs @@ -0,0 +1,113 @@ +use std::sync::mpsc::{self, RecvTimeoutError}; +use std::time::Duration; + +use futures::channel::oneshot; +use scheduler::Instant; + +use super::{HangDetector, HangIncident}; + +/// Detection thresholds and polling cadence for +/// [`crate::App::start_hang_monitor`]. +#[derive(Debug, Clone, Copy)] +pub struct HangMonitorConfig { + /// Duration at which a single piece of foreground work counts as a hang. + pub threshold: Duration, + /// Total foreground spend within one interval that counts as a hang. + pub frame_budget: Duration, + /// How often the monitor thread drains the journal. + pub interval: Duration, +} + +/// Why [`crate::App::start_hang_monitor`] failed. +#[derive(Debug, thiserror::Error)] +pub enum HangMonitorError { + /// The monitor was already started for this app. + #[error("the hang monitor was already started")] + AlreadyStarted, + /// The monitor thread couldn't be spawned. + #[error("failed to spawn the hang monitor thread: {0}")] + Spawn(#[source] std::io::Error), +} + +/// Why a [`HangMonitor`] polled its detector. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HangMonitorPollReason { + /// The monitor's regular interval elapsed. + Interval, + /// A poll was requested before the application quits. Consumers should + /// deliver any batched results now. + Flush, +} + +/// The incidents a [`HangMonitor`] collected in one poll. +pub struct HangMonitorPoll { + /// Incidents sealed since the previous poll, possibly none. + pub incidents: Vec, + /// See [`HangDetector::first_present_at`]. + pub first_present_at: Option, + /// Why this poll happened. + pub reason: HangMonitorPollReason, +} + +enum Request { + Flush { done: oneshot::Sender<()> }, +} + +/// Polls a [`HangDetector`] on a dedicated OS thread. +/// +/// Detection runs off the foreground thread so incidents are still collected +/// and reported while GPUI's executors are stalled. The thread owns the +/// detector and the callback; other threads only send it requests. Dropping +/// the monitor stops the thread. +pub(crate) struct HangMonitor { + requests: mpsc::Sender, +} + +impl HangMonitor { + /// Starts a thread that polls `detector` every `interval` and passes each + /// poll's result, including empty ones, to `on_poll` on that thread. + /// + /// # Errors + /// + /// Returns an error when the thread can't be spawned. + pub(crate) fn spawn( + mut detector: HangDetector, + interval: Duration, + mut on_poll: F, + ) -> std::io::Result + where + F: FnMut(HangMonitorPoll) + Send + 'static, + { + let (requests, receiver) = mpsc::channel(); + std::thread::Builder::new() + .name("HangDetection".to_string()) + .spawn(move || { + loop { + let (reason, done) = match receiver.recv_timeout(interval) { + Ok(Request::Flush { done }) => (HangMonitorPollReason::Flush, Some(done)), + Err(RecvTimeoutError::Timeout) => (HangMonitorPollReason::Interval, None), + Err(RecvTimeoutError::Disconnected) => break, + }; + let incidents = detector.poll(); + on_poll(HangMonitorPoll { + incidents, + first_present_at: detector.first_present_at(), + reason, + }); + if let Some(done) = done { + done.send(()).ok(); + } + } + })?; + Ok(Self { requests }) + } + + /// Asks the monitor thread to poll now with [`HangMonitorPollReason::Flush`] + /// without waiting for it. The returned receiver completes once the + /// callback has returned. Returns `None` if the thread has exited. + pub(crate) fn request_flush(&self) -> Option> { + let (done, finished) = oneshot::channel(); + self.requests.send(Request::Flush { done }).ok()?; + Some(finished) + } +} diff --git a/vendor/gpui-pre/src/profiler/journal.rs b/vendor/gpui-pre/src/profiler/journal.rs new file mode 100644 index 0000000..fba75ae --- /dev/null +++ b/vendor/gpui-pre/src/profiler/journal.rs @@ -0,0 +1,3522 @@ +//! A journal of foreground work and the semantic boundaries between activity intervals. +//! +//! The foreground thread records task polls, action handlers, input dispatches, +//! draws, and presentations as [`ForegroundEvent`]s in a bounded journal ring. +//! Task polls shorter than [`TASK_POLL_FLOOR`] are folded into summaries, which +//! bound the stream by the number of slow polls while preserving their exact +//! count and total duration. +//! +//! Presentation, completed frame skips, and foreground idle are explicit +//! [`IntervalBoundary`] entries in the same stream. Independent [`ForegroundJournalCollector`]s +//! feed entries to [`IntervalSealer`], a pure state machine that groups all work +//! preceding each boundary into a [`FrameSnapshot`]. The sealer does not infer +//! boundaries from elapsed time or from incidental event kinds such as draws. + +use std::cell::{RefCell, UnsafeCell}; +use std::collections::{HashMap, VecDeque}; +use std::mem::MaybeUninit; +use std::sync::{ + Arc, + atomic::{AtomicU64, AtomicUsize, Ordering}, +}; +use std::time::Duration; + +use gpui_util::ResultExt; +use scheduler::Instant; + +use super::{ActionTiming, FrameTiming, PresentTiming, TaskTiming}; +use crate::{App, WindowId, WindowVisibility}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PowerState { + Awake, + Suspended, +} + +/// Task polls shorter than this are folded into a [`PollSummary`] instead of +/// being recorded individually. This keeps the stream bounded by the number +/// of *slow* polls while preserving their exact count and total duration. +pub const TASK_POLL_FLOOR: Duration = Duration::from_micros(100); + +/// A dirty frame that has not been presented by this deadline stops blocking +/// foreground-idle boundaries, so a window that never presents (hidden or +/// occluded windows receive no frame callbacks) cannot suppress boundaries +/// indefinitely. Expiry only unblocks: the open interval still seals at a +/// presentation, completed frame skip, or idle boundary, keeping a starving +/// hang and the callback it starved in one interval. +pub const FRAME_DEADLINE: Duration = Duration::from_secs(1); + +/// Growth in [`system_suspended_time`] below this is attributed to reading +/// its two clocks separately (on Windows both advance only on the system +/// tick, up to 15.6 ms), not to a suspend. A real suspend, including freezing +/// and thawing processes around it, takes far longer. +const MIN_DETECTED_SUSPEND: Duration = Duration::from_millis(50); + +// Backstop against pathological event storms within a single interval. At the +// 100us floor, a fully hung second can produce at most ~10k recordable polls, +// so this bound is only reachable when something is already deeply wrong. +const MAX_INTERVAL_EVENTS: usize = 16 * 1024; + +// Allow 4MiB for the fixed ring allocation, including each slot's atomic +// metadata. The poll floor and frame cadence bound the event rate to roughly +// 10k per second in the worst case, so this holds several seconds of +// worst-case traffic between consumer drains. +const MAX_JOURNAL_ENTRIES: usize = (4 * 1024 * 1024) / core::mem::size_of::(); + +// Absorbs brief collisions with a collector reading the exact slot being +// wrapped. The foreground never waits for a reader; queued entries are retried +// in order on the next publication. +const MAX_PENDING_JOURNAL_ENTRIES: usize = 64; + +/// One entry in the foreground stream. +/// +/// Events are recorded in completion order (ordered by their end time). An +/// event that was in progress across a frame boundary (e.g. the task poll +/// enclosing a draw) is recorded when it *ends*, with timestamps that may +/// precede events recorded before it. +#[derive(Debug, Copy, Clone)] +pub enum ForegroundEvent { + /// A foreground task poll at least [`TASK_POLL_FLOOR`] long. + TaskPoll(TaskTiming), + /// A completed action handler. + Action(ActionTiming), + /// A dispatched platform input event. + Input(InputTiming), + /// A completed window draw. + Draw(FrameTiming), + /// Work spent submitting a frame to the platform. + Present(PresentTiming), + /// Aggregate of task polls below [`TASK_POLL_FLOOR`], flushed before the + /// next individually retained event or interval boundary. + SmallPolls(SmallPollFlush), +} + +impl ForegroundEvent { + /// When the work described by this event began. For draws this is the + /// start of the draw itself, not the frame's first invalidation. + pub fn start_time(&self) -> Instant { + match self { + Self::TaskPoll(timing) => timing.start, + Self::Action(timing) => timing.start, + Self::Input(timing) => timing.start, + Self::Draw(timing) => timing.draw_start, + Self::Present(timing) => timing.present_start, + Self::SmallPolls(flush) => flush.since, + } + } + + /// When the work described by this event ended. + pub fn end_time(&self) -> Instant { + match self { + Self::TaskPoll(timing) => timing.end.0, + Self::Action(timing) => timing.end, + Self::Input(timing) => timing.end, + Self::Draw(timing) => timing.draw_end, + Self::Present(timing) => timing.present_end, + Self::SmallPolls(flush) => flush.until, + } + } + + /// How long the work described by this event took. For + /// [`Self::SmallPolls`] this is the span the summary covers, not time + /// spent polling; use the summary's `total` for occupancy. + pub fn duration(&self) -> Duration { + self.end_time().duration_since(self.start_time()) + } +} + +/// Timing of one platform input dispatch on a window. +#[derive(Debug, Copy, Clone)] +pub struct InputTiming { + /// The platform input variant dispatched (see + /// [`crate::PlatformInput::kind_name`]). + pub kind: &'static str, + /// When the input dispatch started. + pub start: Instant, + /// When the input dispatch finished. + pub end: Instant, + /// Whether handling the input invalidated a window. + pub caused_invalidation: bool, +} + +/// A frame callback entering the foreground, without implying foreground work. +#[derive(Debug, Copy, Clone)] +pub struct PlatformSignal { + /// The window receiving the callback. + pub window_id: WindowId, + /// When the platform requested the callback, if its clock is supported. + pub signal_at: Option, + /// When foreground handling began. + pub handled_at: Instant, + /// Whether the callback came from the platform or local scheduling. + pub source: crate::FrameRequestSource, +} + +/// Why a completed frame request did not draw a new frame. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FrameSkipReason { + /// The callback completed without needing a new render. + NoRenderNeeded, + /// An inactive window deferred drawing. + InactiveFrameRateLimit, + /// Thermal throttling deferred drawing. + ThermalFrameRateLimit, +} + +/// A completed frame request that returned without drawing a new frame. +#[derive(Debug, Copy, Clone)] +pub struct FrameSkipped { + /// The window whose callback returned. + pub window_id: WindowId, + /// When the foreground completed this callback. + pub at: Instant, + /// Why no new frame was drawn. + pub reason: FrameSkipReason, +} + +/// Exact count and total duration of task polls below [`TASK_POLL_FLOOR`]. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct PollSummary { + /// Number of polls below the floor. + pub count: u64, + /// Total duration of polls below the floor. + pub total: Duration, +} + +impl PollSummary { + fn add(&mut self, other: Self) { + self.count += other.count; + self.total += other.total; + } +} + +/// A flushed [`PollSummary`] and the tightest span containing its folded polls. +#[derive(Debug, Copy, Clone)] +pub struct SmallPollFlush { + /// The folded polls. + pub summary: PollSummary, + /// When the first folded poll began. + pub since: Instant, + /// When the last folded poll ended. + pub until: Instant, +} + +/// A newly drawn frame and the platform submission that completed its interval. +#[derive(Debug, Copy, Clone)] +pub struct PresentedFrame { + /// The draw whose rendered scene was submitted. + pub frame: FrameTiming, + /// Work spent submitting that scene to the platform. + pub presentation: PresentTiming, +} + +impl PresentedFrame { + /// Time from the frame's first invalidation through platform submission. + pub fn dirty_to_present_duration(&self) -> Option { + self.frame + .dirty_at + .map(|dirty_at| self.presentation.present_end.duration_since(dirty_at)) + } +} + +/// The semantic event that completed a foreground activity interval. +#[derive(Debug, Copy, Clone)] +pub enum IntervalBoundary { + /// A newly drawn frame was submitted to the platform. + Presented(PresentedFrame), + /// A callback completed without drawing, excluding reentrant callbacks. + FrameSkipped(FrameSkipped), + /// The foreground returned to an idle platform loop with no unexpired + /// frame pending. + Idle { + /// When the foreground went idle. + ended_at: Instant, + }, + /// A system suspend interrupted the measurement interval, observed either + /// through a power notification or through the suspend clock. + PowerTransition { + /// When the foreground noticed the suspend, through a notification or + /// the suspend clock. + ended_at: Instant, + }, +} + +impl IntervalBoundary { + /// When the interval ended. + pub fn end_time(&self) -> Instant { + match self { + Self::Presented(presented) => presented.presentation.present_end, + Self::FrameSkipped(skipped) => skipped.at, + Self::Idle { ended_at } | Self::PowerTransition { ended_at } => *ended_at, + } + } + + /// The first invalidation of the frame satisfied by this boundary, if any. + pub fn dirty_at(&self) -> Option { + match self { + Self::Presented(presented) => presented.frame.dirty_at, + Self::FrameSkipped(_) | Self::Idle { .. } | Self::PowerTransition { .. } => None, + } + } +} + +/// A control-plane change to one window's pending-frame state. +#[derive(Debug, Copy, Clone)] +pub enum FrameStateChange { + /// The window has a frame waiting to be presented. + Pending { + /// The window waiting for presentation. + window_id: WindowId, + /// When this frame generation first became dirty. + dirty_at: Instant, + }, + /// The window closed, so any pending frame no longer blocks idle + /// boundaries. + Closed { + /// The window that closed. + window_id: WindowId, + /// When the window closed. + at: Instant, + }, +} + +/// One item retained in the foreground journal. +#[derive(Debug, Copy, Clone)] +pub enum ForegroundJournalEntry { + /// A completed piece of foreground work. + Event(ForegroundEvent), + /// A semantic interval boundary. + Boundary(IntervalBoundary), + /// A change to pending-frame state. This is metadata, not foreground work. + FrameState(FrameStateChange), + /// A callback entering the foreground; not work or an interval boundary. + PlatformSignal(PlatformSignal), + /// A nested frame request deferred while an enclosing draw was still running. + ReentrantFrameSkipped { + /// The window whose nested frame request was deferred. + window_id: WindowId, + /// When the nested frame request was deferred. + at: Instant, + }, + /// One or more logical entries were unavailable at this point in the + /// stream. Consumers must not infer interval boundaries across this gap. + Discontinuity { + /// Number of unavailable logical entries. + lost: u64, + }, +} + +/// An immutable view of one sealed foreground interval, produced by +/// [`IntervalSealer`]. +#[derive(Debug, Clone)] +pub struct FrameSnapshot { + /// When the interval started (the previous seal, or the end of the idle + /// stretch preceding the interval's first event). + pub interval_start: Instant, + /// The semantic event that completed the interval, including its metadata. + pub boundary: IntervalBoundary, + /// Foreground work recorded during the interval, in completion order. + /// [`ForegroundEvent::SmallPolls`] entries are collected into + /// `small_polls` instead of appearing here. + pub events: Vec, + /// Span-tagged aggregates of task polls below [`TASK_POLL_FLOOR`]. Spans + /// are retained so occupancy can apportion folded poll time to reporting + /// windows narrower than the interval. + pub small_polls: Vec, + /// Events lost to the interval's event cap, plus ring losses reported + /// via [`IntervalSealer::note_lost`]. + pub dropped_events: u64, + /// Whether the journal had an unobserved gap during this interval. + pub journal_discontinuous: bool, +} + +impl FrameSnapshot { + /// When the interval ended. + pub fn interval_end(&self) -> Instant { + self.boundary.end_time() + } + + /// The combined count and total of all folded sub-floor polls in the + /// interval. + pub fn small_poll_summary(&self) -> PollSummary { + let mut summary = PollSummary::default(); + for flush in &self.small_polls { + summary.add(flush.summary); + } + summary + } + + /// Total foreground time occupied within the interval: the union of the + /// recorded events' spans (clamped to the interval, so nested work like an + /// action inside an input dispatch is not double counted) plus the folded + /// small polls. Folded polls cannot be unioned with nested individually + /// timed work, so this remains a close approximation. + pub fn occupancy(&self) -> Duration { + self.occupancy_within(self.interval_start, self.interval_end()) + } + + /// Like [`Self::occupancy`], but measured against an arbitrary window + /// (e.g. a reporting window anchored at a frame's first invalidation). + /// Event spans are clamped to the window. Folded polls carry no + /// individual timestamps, so each flush's total is apportioned by how + /// much of its span overlaps the window — assuming the folded time is + /// spread uniformly across the span. + pub fn occupancy_within(&self, window_start: Instant, window_end: Instant) -> Duration { + let mut spans: Vec<(Instant, Instant)> = self + .events + .iter() + .map(|event| { + let start = event.start_time().max(window_start); + let end = event.end_time().min(window_end).max(start); + (start, end) + }) + .collect(); + spans.sort_by_key(|(start, _)| *start); + + let mut occupied = Duration::ZERO; + let mut merged_until: Option = None; + for (start, end) in spans { + let start = match merged_until { + Some(merged_until) => start.max(merged_until), + None => start, + }; + occupied += end.duration_since(start); + merged_until = Some(match merged_until { + Some(merged_until) => merged_until.max(end), + None => end, + }); + } + + let apportioned_small_polls: Duration = self + .small_polls + .iter() + .map(|flush| { + let span = flush.until.duration_since(flush.since); + if span.is_zero() { + // A point-like flush lies either inside or outside the + // window. + if flush.since >= window_start && flush.since <= window_end { + flush.summary.total + } else { + Duration::ZERO + } + } else { + let overlap_start = flush.since.max(window_start); + let overlap_end = flush.until.min(window_end).max(overlap_start); + let overlap = overlap_end.duration_since(overlap_start); + flush.summary.total.mul_f64(overlap.div_duration_f64(span)) + } + }) + .sum(); + occupied + apportioned_small_polls + } + + /// The fraction of the interval the foreground spent working, in `0.0..=1.0`. + pub fn busy_fraction(&self) -> f64 { + let interval = self.interval_end().duration_since(self.interval_start); + if interval.is_zero() { + return 1.0; + } + (self.occupancy().div_duration_f64(interval)).min(1.0) + } +} + +#[derive(Clone)] +pub(crate) struct ForegroundRunnableCounter(Arc); + +impl ForegroundRunnableCounter { + fn new() -> Self { + Self(Arc::new(AtomicUsize::new(0))) + } + + pub(crate) fn queued(&self) { + self.0.fetch_add(1, Ordering::Release); + } + + fn finished(&self) { + let _ = self + .0 + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| { + count.checked_sub(1) + }); + } + + fn has_runnables(&self) -> bool { + self.0.load(Ordering::Acquire) > 0 + } +} + +// Visibility and power changes clear `dirty_at`; `Window::refresh_visibility` +// re-records the frame from the window's actual dirty state afterwards. +struct WindowFrameState { + visibility: WindowVisibility, + dirty_at: Option, + visibility_changed_at: Option, +} + +impl WindowFrameState { + fn new(visibility: WindowVisibility) -> Self { + Self { + visibility, + dirty_at: None, + visibility_changed_at: None, + } + } +} + +struct ForegroundJournalWriter { + foreground_runnables: ForegroundRunnableCounter, + publisher: JournalPublisher, + turn_depth: usize, + windows: HashMap, + power: PowerState, + power_changed_at: Option, + read_suspended_time: fn() -> Option, + suspended_time: Option, + retained_since_boundary: bool, + small_polls: Option, +} + +impl ForegroundJournalWriter { + fn new(foreground_runnables: ForegroundRunnableCounter, publisher: JournalPublisher) -> Self { + Self { + foreground_runnables, + publisher, + turn_depth: 0, + windows: HashMap::new(), + power: PowerState::Awake, + power_changed_at: None, + read_suspended_time: system_suspended_time, + suspended_time: system_suspended_time(), + retained_since_boundary: false, + small_polls: None, + } + } + + fn begin_turn(&mut self) { + self.turn_depth += 1; + } + + // Idle boundaries require a retained event since the last boundary, not + // merely folded sub-floor polls. Sporadic wake-ups (timers, file + // watchers) leave the foreground idle after every tiny poll; a boundary + // for each would re-admit to the ring the very polls the fold keeps out + // of it, wrapping the ring within seconds. Folded-only work is discarded + // at true idle so unrelated wake-ups cannot accumulate toward a later + // frame budget. Polls remain folded while a frame or runnable is pending. + fn end_turn(&mut self, ended_at: Instant) { + let Some(turn_depth) = self.turn_depth.checked_sub(1) else { + debug_assert!(false, "foreground turn must be begun before it ends"); + return; + }; + self.turn_depth = turn_depth; + if self.turn_depth > 0 + || self.foreground_runnables.has_runnables() + || self.has_unexpired_pending_frame(ended_at) + { + return; + } + if !self.retained_since_boundary { + self.small_polls = None; + return; + } + + self.record_entry(ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { + ended_at, + })); + } + + fn has_unexpired_pending_frame(&mut self, now: Instant) -> bool { + for window in self.windows.values_mut() { + if window + .dirty_at + .is_some_and(|at| now.saturating_duration_since(at) >= FRAME_DEADLINE) + { + window.dirty_at = None; + } + } + self.power == PowerState::Awake + && self + .windows + .values() + .any(|window| window.dirty_at.is_some()) + } + + fn power_transition(&mut self, power: PowerState, at: Instant) { + if self.power == power { + return; + } + self.power = power; + // A suspend that was notified after it happened must not be reported + // again by the suspend clock. A notification that precedes its + // suspend (macOS posts will-sleep up to 30 s early) leaves the clock + // to fire once more after wake, which only seals an empty interval. + self.suspended_time = (self.read_suspended_time)(); + self.interrupt_measurements(at); + } + + /// Interrupts measurements, as a power notification would, if the system + /// suspended since the previous check. Measurements that end call this + /// before deciding whether to keep themselves, so a suspend during them + /// is detected even if its notification hasn't been handled yet. + // + // Power notifications arrive on the foreground thread, so work that was + // running when the system suspended finishes before its notification is + // handled (and on Windows the notification may never arrive). + fn detect_suspend(&mut self, at: Instant) { + let Some(suspended_time) = (self.read_suspended_time)() else { + return; + }; + let previous = self.suspended_time.replace(suspended_time); + if previous + .is_some_and(|previous| suspended_time.saturating_sub(previous) >= MIN_DETECTED_SUSPEND) + { + self.interrupt_measurements(at); + } + } + + /// Seals the open interval and invalidates all measurements in progress + /// at `at`, since they may span a suspend. + fn interrupt_measurements(&mut self, at: Instant) { + self.record_entry(ForegroundJournalEntry::Boundary( + IntervalBoundary::PowerTransition { ended_at: at }, + )); + self.power_changed_at = Some(at); + for window in self.windows.values_mut() { + window.dirty_at = None; + } + } + + /// Whether work that started at `start` spans a sleep or a wake, and so + /// must not be measured as uninterrupted foreground work. + fn power_interrupted_since(&self, start: Instant) -> bool { + self.power == PowerState::Suspended || self.power_changed_at.is_some_and(|at| start <= at) + } + + fn set_visibility(&mut self, id: WindowId, visibility: WindowVisibility, at: Instant) { + let window = self + .windows + .entry(id) + .or_insert_with(|| WindowFrameState::new(visibility)); + if window.visibility != visibility { + window.visibility = visibility; + window.visibility_changed_at = Some(at); + window.dirty_at = None; + } + } + + fn fold_small_poll(&mut self, timing: TaskTiming) { + let flush = self.small_polls.get_or_insert(SmallPollFlush { + summary: PollSummary::default(), + since: timing.start, + until: timing.end.0, + }); + flush.summary.count += 1; + flush.summary.total += timing.poll_duration(); + flush.since = flush.since.min(timing.start); + flush.until = flush.until.max(timing.end.0); + } + + fn take_small_polls(&mut self) -> Option { + self.small_polls.take() + } + + fn record_event(&mut self, event: ForegroundEvent) { + self.retained_since_boundary = true; + self.record_entry(ForegroundJournalEntry::Event(event)); + } + + fn record_entry(&mut self, entry: ForegroundJournalEntry) { + let small_polls = self + .take_small_polls() + .map(ForegroundEvent::SmallPolls) + .map(ForegroundJournalEntry::Event); + self.publisher + .publish([small_polls, Some(entry)].into_iter().flatten()); + if matches!(entry, ForegroundJournalEntry::Boundary(_)) { + self.retained_since_boundary = false; + } + } + + /// Publishes a point record that is neither work nor a boundary. Folded + /// polls stay folded: publishing them here would let them reach the + /// sealer even when [`Self::end_turn`] later discards folded-only work. + fn record_marker(&mut self, entry: ForegroundJournalEntry) { + self.publisher.publish([entry]); + } + + fn record_frame_state(&mut self, change: FrameStateChange) { + self.record_entry(ForegroundJournalEntry::FrameState(change)); + } + + fn record_platform_signal(&mut self, signal: PlatformSignal) { + self.record_marker(ForegroundJournalEntry::PlatformSignal(signal)); + } + + /// Seals retained work at a frame request that didn't draw. Like idle + /// boundaries, skips require a retained event since the last boundary: + /// platforms that request a frame every refresh would otherwise seal an + /// empty interval, and flush folded polls, on every idle refresh. + fn record_frame_skipped(&mut self, skipped: FrameSkipped) { + if !self.retained_since_boundary { + return; + } + // Even a no-render callback can schedule or invalidate the next frame. + // Skipping does not establish that the window's pending demand is met. + self.record_entry(ForegroundJournalEntry::Boundary( + IntervalBoundary::FrameSkipped(skipped), + )); + } + + fn record_frame_pending(&mut self, window_id: WindowId, dirty_at: Instant) { + let window = self + .windows + .entry(window_id) + .or_insert_with(|| WindowFrameState::new(WindowVisibility::Visible)); + if self.power == PowerState::Suspended + || !window.visibility.is_visible() + || window + .dirty_at + .is_some_and(|at| dirty_at.saturating_duration_since(at) < FRAME_DEADLINE) + { + return; + } + + window.dirty_at = Some(dirty_at); + self.record_frame_state(FrameStateChange::Pending { + window_id, + dirty_at, + }); + } + + fn record_window_closed(&mut self, window_id: WindowId, at: Instant) { + self.windows.remove(&window_id); + self.record_frame_state(FrameStateChange::Closed { window_id, at }); + } + + fn record_present(&mut self, timing: PresentTiming, frame: Option) { + if let Some(window) = self.windows.get_mut(&timing.window_id) { + window.dirty_at = None; + } + if self.power_interrupted_since(timing.present_start) { + return; + } + match frame { + Some(frame) => { + self.record_entry(ForegroundJournalEntry::Boundary( + IntervalBoundary::Presented(PresentedFrame { + frame, + presentation: timing, + }), + )); + } + None => { + self.record_event(ForegroundEvent::Present(timing)); + } + } + } +} + +thread_local! { + static FOREGROUND_RUNNABLES: ForegroundRunnableCounter = ForegroundRunnableCounter::new(); + static FOREGROUND_JOURNAL: RefCell> = const { RefCell::new(None) }; +} + +pub(crate) fn foreground_runnable_counter() -> ForegroundRunnableCounter { + FOREGROUND_RUNNABLES.with(Clone::clone) +} + +/// Starts journaling on the calling thread. Called once by `App` construction +/// on the main thread; every other thread's recording calls are no-ops. +/// Idempotent so that multiple `App`s on one thread (tests) share one journal. +pub(crate) fn install_foreground_journal() -> ForegroundJournal { + let foreground_runnables = foreground_runnable_counter(); + FOREGROUND_JOURNAL.with(|journal| { + let mut journal = journal.borrow_mut(); + if let Some(journal) = journal.as_ref() { + return ForegroundJournal { + ring: Arc::clone(&journal.publisher.ring), + }; + } + + let (handle, publisher) = + ForegroundJournal::new(MAX_JOURNAL_ENTRIES, MAX_PENDING_JOURNAL_ENTRIES); + *journal = Some(ForegroundJournalWriter::new( + foreground_runnables, + publisher, + )); + handle + }) +} + +/// Restores the previous foreground journal on drop, so a test's writer +/// (turn depth, pending frames, retained-since-boundary state) never leaks +/// into a later test that happens to reuse the same test-harness thread. +#[cfg(test)] +pub(crate) struct TestForegroundJournalGuard { + previous: Option, + _not_send: std::marker::PhantomData>, +} + +#[cfg(test)] +impl Drop for TestForegroundJournalGuard { + fn drop(&mut self) { + FOREGROUND_JOURNAL.with(|journal| { + *journal.borrow_mut() = self.previous.take(); + }); + } +} + +/// Installs a fresh journal for the duration of a test, isolated from +/// whatever a previous test left on this thread. Used by tests outside this +/// module (e.g. `bench_context`) that need to observe journal entries +/// without interference from the shared per-thread journal. +#[cfg(test)] +pub(crate) fn install_test_foreground_journal( + capacity: usize, + pending_capacity: usize, +) -> (ForegroundJournal, TestForegroundJournalGuard) { + let foreground_runnables = foreground_runnable_counter(); + let (handle, publisher) = ForegroundJournal::new(capacity, pending_capacity); + let previous = FOREGROUND_JOURNAL.with(|journal| { + journal.borrow_mut().replace( + ForegroundJournalWriter::new(foreground_runnables, publisher).with_test_suspend_clock(), + ) + }); + ( + handle, + TestForegroundJournalGuard { + previous, + _not_send: std::marker::PhantomData, + }, + ) +} + +#[cfg(test)] +thread_local! { + static TEST_SUSPENDED_TIME: std::cell::Cell = const { std::cell::Cell::new(Duration::ZERO) }; +} + +#[cfg(test)] +fn test_suspended_time() -> Option { + Some(TEST_SUSPENDED_TIME.get()) +} + +/// Simulates the system spending `duration` suspended, as observed through +/// the suspend clock of journals using [`ForegroundJournalWriter::with_test_suspend_clock`]. +#[cfg(test)] +pub(crate) fn advance_test_suspended_time(duration: Duration) { + TEST_SUSPENDED_TIME.set(TEST_SUSPENDED_TIME.get() + duration); +} + +#[cfg(test)] +impl ForegroundJournalWriter { + /// Replaces the system suspend clock, so a real suspend during a test + /// can't interrupt its measurements. + fn with_test_suspend_clock(mut self) -> Self { + TEST_SUSPENDED_TIME.set(Duration::ZERO); + self.read_suspended_time = test_suspended_time; + self.suspended_time = test_suspended_time(); + self + } +} + +fn with_journal(f: impl FnOnce(&mut ForegroundJournalWriter)) { + FOREGROUND_JOURNAL.with(|journal| { + if let Some(journal) = journal.borrow_mut().as_mut() { + f(journal); + } + }); +} + +pub(crate) fn observe_power(cx: &App) { + cx.on_system_sleep(|_| { + record_power_transition(PowerState::Suspended); + }) + .detach(); + // Visibility notifications delivered during sleep may have been missed. + cx.on_system_wake(|cx| { + record_power_transition(PowerState::Awake); + cx.spawn(async |cx| { + cx.update(|cx| { + for handle in cx.windows() { + handle + .update(cx, |_, window, cx| window.refresh_visibility(cx)) + .log_err(); + } + }); + }) + .detach(); + }) + .detach(); +} + +/// Total time the system has spent suspended since boot, or `None` where no +/// clock provides it: the difference between a clock that counts suspended +/// time and one that doesn't. The suspend-excluding clock is read first so +/// read latency can't make the difference negative. Every read is served from +/// memory the kernel maps into the process, without a syscall. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn system_suspended_time() -> Option { + // CLOCK_BOOTTIME is CLOCK_MONOTONIC plus time spent suspended: + // https://man7.org/linux/man-pages/man2/clock_gettime.2.html + let awake = read_clock(libc::CLOCK_MONOTONIC)?; + let total = read_clock(libc::CLOCK_BOOTTIME)?; + Some(total.saturating_sub(awake)) +} + +#[cfg(target_os = "macos")] +fn system_suspended_time() -> Option { + use std::sync::OnceLock; + + // Declared here because libc deprecates its mach bindings, and these + // three functions don't justify a `mach2` dependency. `` + // documents `mach_continuous_time` as "like mach_absolute_time, but + // advances during sleep", so both count the same ticks: + // https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/mach/mach_time.h + #[repr(C)] + struct MachTimebaseInfo { + numer: u32, + denom: u32, + } + unsafe extern "C" { + fn mach_absolute_time() -> u64; + fn mach_continuous_time() -> u64; + fn mach_timebase_info(info: *mut MachTimebaseInfo) -> i32; + } + + static TIMEBASE: OnceLock> = OnceLock::new(); + let (numer, denom) = (*TIMEBASE.get_or_init(|| { + let mut info = MachTimebaseInfo { numer: 0, denom: 0 }; + // SAFETY: `info` is a valid, writable mach_timebase_info. + let succeeded = unsafe { mach_timebase_info(&mut info) } == 0; + (succeeded && info.denom != 0).then_some((info.numer, info.denom)) + }))?; + // SAFETY: neither function has preconditions. + let awake = unsafe { mach_absolute_time() }; + // SAFETY: as above. + let total = unsafe { mach_continuous_time() }; + let nanos = u128::from(total.saturating_sub(awake)) * u128::from(numer) / u128::from(denom); + Some(Duration::from_nanos(u64::try_from(nanos).ok()?)) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn read_clock(clock: libc::clockid_t) -> Option { + let mut time = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + // SAFETY: `time` is a valid, writable timespec. + if unsafe { libc::clock_gettime(clock, &mut time) } != 0 { + return None; + } + Some(Duration::new( + u64::try_from(time.tv_sec).ok()?, + u32::try_from(time.tv_nsec).ok()?, + )) +} + +#[cfg(target_os = "windows")] +fn system_suspended_time() -> Option { + use windows::Win32::System::WindowsProgramming::{ + QueryInterruptTime, QueryUnbiasedInterruptTime, + }; + let mut awake = 0; + // SAFETY: `awake` is a valid, writable u64. + if !unsafe { QueryUnbiasedInterruptTime(&mut awake) }.as_bool() { + return None; + } + // SAFETY: no preconditions. + let total = unsafe { QueryInterruptTime() }; + // Both count 100 ns units; unbiased interrupt time excludes sleep and + // hibernation: + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/interrupt-time + Some(Duration::from_nanos( + total.saturating_sub(awake).saturating_mul(100), + )) +} + +#[cfg(not(any( + target_os = "linux", + target_os = "android", + target_os = "macos", + target_os = "windows" +)))] +fn system_suspended_time() -> Option { + None +} + +pub(crate) fn record_power_transition(state: PowerState) { + with_journal(|journal| journal.power_transition(state, Instant::now())); +} + +/// Whether work that started at `start` spans a sleep or a wake. Such work +/// is dropped rather than measured. +pub(crate) fn power_interrupted_since(start: Instant) -> bool { + let mut interrupted = false; + with_journal(|journal| { + journal.detect_suspend(Instant::now()); + interrupted = journal.power_interrupted_since(start); + }); + interrupted +} + +pub(crate) fn record_window_visibility(id: WindowId, visibility: WindowVisibility) { + with_journal(|journal| journal.set_visibility(id, visibility, Instant::now())); +} + +/// Whether a frame sample that started at `start` was measured while the +/// window stayed visible and the system stayed awake. +pub(crate) fn frame_sample_is_valid(id: WindowId, start: Instant) -> bool { + let mut valid = true; + with_journal(|journal| { + journal.detect_suspend(Instant::now()); + valid = !journal.power_interrupted_since(start) + && journal.windows.get(&id).is_none_or(|window| { + window.visibility.is_visible() + && window.visibility_changed_at.is_none_or(|at| start > at) + }); + }); + valid +} + +// TODO(gpui-profiler): the turn brackets in the dispatchers and +// WindowProfiler are bare begin/end call pairs rather than uses of this +// guard. A caught unwind between a pair leaves `turn_depth` (and potentially +// the runnable counter) permanently unbalanced, which silently disables +// idle boundaries for the rest of the process. Zed aborts on panics so +// this is latent today, but gpui-as-a-library callers may catch unwinds; +// route all bracketing through this guard. +pub(crate) struct ForegroundTurnGuard; + +impl Drop for ForegroundTurnGuard { + fn drop(&mut self) { + end_foreground_turn(); + } +} + +pub(crate) fn foreground_turn() -> ForegroundTurnGuard { + begin_foreground_turn(); + ForegroundTurnGuard +} + +pub(crate) fn begin_foreground_turn() { + // Observing before the turn's work takes its start time means a suspend + // detected here never invalidates the work that follows it. + with_journal(|journal| { + journal.detect_suspend(Instant::now()); + journal.begin_turn(); + }); +} + +pub(crate) fn end_foreground_turn() { + with_journal(|journal| journal.end_turn(Instant::now())); +} + +pub(crate) fn record_task_poll(timing: TaskTiming) { + FOREGROUND_RUNNABLES.with(ForegroundRunnableCounter::finished); + with_journal(|journal| { + journal.detect_suspend(timing.end.0); + if journal.power_interrupted_since(timing.start) { + journal.end_turn(timing.end.0); + return; + } + if timing.poll_duration() >= TASK_POLL_FLOOR { + journal.record_event(ForegroundEvent::TaskPoll(timing)); + } else { + journal.fold_small_poll(timing); + } + journal.end_turn(timing.end.0); + }); +} + +pub(crate) fn record_action(timing: ActionTiming) { + with_journal(|journal| journal.record_event(ForegroundEvent::Action(timing))); +} + +pub(crate) fn record_input(timing: InputTiming) { + with_journal(|journal| journal.record_event(ForegroundEvent::Input(timing))); +} + +pub(crate) fn record_draw(timing: FrameTiming) { + with_journal(|journal| journal.record_event(ForegroundEvent::Draw(timing))); +} + +pub(crate) fn record_present(timing: PresentTiming, frame: Option) { + with_journal(|journal| journal.record_present(timing, frame)); +} + +pub(crate) fn record_platform_signal(signal: PlatformSignal) { + with_journal(|journal| journal.record_platform_signal(signal)); +} + +pub(crate) fn record_reentrant_frame_skipped(window_id: WindowId, at: Instant) { + with_journal(|journal| { + journal.record_marker(ForegroundJournalEntry::ReentrantFrameSkipped { window_id, at }); + }); +} + +pub(crate) fn record_frame_skipped(window_id: WindowId, at: Instant, reason: FrameSkipReason) { + with_journal(|journal| { + journal.record_frame_skipped(FrameSkipped { + window_id, + at, + reason, + }) + }); +} + +pub(crate) fn record_frame_pending(window_id: WindowId, dirty_at: Instant) { + with_journal(|journal| journal.record_frame_pending(window_id, dirty_at)); +} + +pub(crate) fn record_window_closed(window_id: WindowId) { + let at = Instant::now(); + with_journal(|journal| journal.record_window_closed(window_id, at)); +} + +const SLOT_WRITER: usize = 1 << (usize::BITS - 1); +const SLOT_READER_MASK: usize = !SLOT_WRITER; +const EMPTY_SEQUENCE: u64 = u64::MAX; + +struct JournalSlot { + users: AtomicUsize, + sequence: AtomicU64, + entry: UnsafeCell>, +} + +// `users` ensures the entry is only read while no writer owns the slot and is +// only written while no readers own it. +unsafe impl Sync for JournalSlot {} + +impl JournalSlot { + fn new() -> Self { + Self { + users: AtomicUsize::new(0), + sequence: AtomicU64::new(EMPTY_SEQUENCE), + entry: UnsafeCell::new(MaybeUninit::uninit()), + } + } + + fn try_publish(&self, sequence: u64, entry: ForegroundJournalEntry) -> bool { + if self + .users + .compare_exchange(0, SLOT_WRITER, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + return false; + } + + // SAFETY: setting SLOT_WRITER from zero gives this writer exclusive + // access to the slot until the Release store below. + unsafe { + (*self.entry.get()).write(entry); + } + self.sequence.store(sequence, Ordering::Relaxed); + self.users.store(0, Ordering::Release); + true + } + + fn try_read(&self, expected_sequence: u64) -> Option { + let _guard = JournalSlotReadGuard::try_new(self)?; + if self.sequence.load(Ordering::Relaxed) != expected_sequence { + return None; + } + + // SAFETY: a matching sequence means the slot was initialized for this + // logical entry, and `guard` prevents the writer from overwriting it + // until after the Copy. + Some(unsafe { *(*self.entry.get()).assume_init_ref() }) + } + + fn try_add_reader(&self) -> bool { + let mut users = self.users.load(Ordering::Relaxed); + loop { + if users & SLOT_WRITER != 0 || users == SLOT_READER_MASK { + return false; + } + match self.users.compare_exchange_weak( + users, + users + 1, + Ordering::Acquire, + Ordering::Relaxed, + ) { + Ok(_) => return true, + Err(updated_users) => users = updated_users, + } + } + } + + fn remove_reader(&self) { + let previous = self.users.fetch_sub(1, Ordering::Release); + debug_assert!( + previous > 0 && previous & SLOT_WRITER == 0, + "invalid slot reader state: {previous:#x}" + ); + } +} + +struct JournalSlotReadGuard<'a> { + slot: &'a JournalSlot, +} + +impl<'a> JournalSlotReadGuard<'a> { + fn try_new(slot: &'a JournalSlot) -> Option { + if slot.try_add_reader() { + Some(Self { slot }) + } else { + None + } + } +} + +impl Drop for JournalSlotReadGuard<'_> { + fn drop(&mut self) { + self.slot.remove_reader(); + } +} + +struct JournalRing { + slots: Box<[JournalSlot]>, + finalized: AtomicU64, + offered: AtomicU64, +} + +impl JournalRing { + fn new(capacity: usize) -> Self { + let capacity = capacity.max(1); + Self { + slots: (0..capacity).map(|_| JournalSlot::new()).collect(), + finalized: AtomicU64::new(0), + offered: AtomicU64::new(0), + } + } + + fn capacity(&self) -> usize { + self.slots.len() + } + + fn try_publish(&self, sequence: u64, entry: ForegroundJournalEntry) -> bool { + let index = (sequence % self.slots.len() as u64) as usize; + self.slots[index].try_publish(sequence, entry) + } + + fn read(&self, sequence: u64) -> Option { + let index = (sequence % self.slots.len() as u64) as usize; + self.slots[index].try_read(sequence) + } +} + +struct PendingJournalEntry { + sequence: u64, + entry: ForegroundJournalEntry, +} + +struct JournalPublisher { + ring: Arc, + next_sequence: u64, + pending: VecDeque, + dropped_after_pending: u64, + pending_capacity: usize, +} + +impl JournalPublisher { + fn new(ring: Arc, pending_capacity: usize) -> Self { + Self { + ring, + next_sequence: 0, + pending: VecDeque::with_capacity(pending_capacity), + dropped_after_pending: 0, + pending_capacity, + } + } + + fn publish(&mut self, entries: impl IntoIterator) { + for entry in entries { + self.flush_pending(); + self.publish_one(entry); + } + self.flush_pending(); + } + + fn publish_one(&mut self, entry: ForegroundJournalEntry) { + let sequence = self.next_sequence; + self.next_sequence += 1; + self.ring + .offered + .store(self.next_sequence, Ordering::Release); + + if !self.pending.is_empty() || self.dropped_after_pending > 0 { + if self.dropped_after_pending == 0 && self.pending.len() < self.pending_capacity { + self.pending + .push_back(PendingJournalEntry { sequence, entry }); + } else { + self.dropped_after_pending += 1; + } + return; + } + + if self.ring.try_publish(sequence, entry) { + self.ring.finalized.store(sequence + 1, Ordering::Release); + } else if self.pending_capacity > 0 { + self.pending + .push_back(PendingJournalEntry { sequence, entry }); + } else { + self.dropped_after_pending = 1; + } + } + + fn flush_pending(&mut self) { + while let Some(pending) = self.pending.front() { + if !self.ring.try_publish(pending.sequence, pending.entry) { + return; + } + let sequence = pending.sequence; + self.pending.pop_front(); + self.ring.finalized.store(sequence + 1, Ordering::Release); + } + + if self.dropped_after_pending > 0 { + self.ring + .finalized + .store(self.next_sequence, Ordering::Release); + self.dropped_after_pending = 0; + } + } +} + +/// A cloneable handle to one foreground journal stream. +/// +/// Each collector has an independent cursor. Collectors briefly pin one slot +/// at a time and never block recording; entries they could not observe are +/// reported as discontinuities. Apps on the same foreground thread share the +/// stream. +#[derive(Clone)] +pub struct ForegroundJournal { + ring: Arc, +} + +impl ForegroundJournal { + fn new(capacity: usize, pending_capacity: usize) -> (Self, JournalPublisher) { + let ring = Arc::new(JournalRing::new(capacity)); + ( + Self { + ring: Arc::clone(&ring), + }, + JournalPublisher::new(ring, pending_capacity), + ) + } + + /// Creates an independent collector that observes entries offered after + /// this call. + pub fn collector(&self) -> ForegroundJournalCollector { + ForegroundJournalCollector { + cursor: self.ring.offered.load(Ordering::Acquire), + ring: Arc::clone(&self.ring), + } + } +} + +/// Entries returned by one [`ForegroundJournalCollector::collect_unseen`] call. +#[derive(Debug, Default)] +pub struct DrainedEntries { + /// Journal entries recorded since the previous drain, in recording order, + /// including synthetic [`ForegroundJournalEntry::Discontinuity`] markers at + /// unavailable logical positions. + pub entries: Vec, + /// Entries unavailable to this collector because they were overwritten or + /// could not be published after the fixed pending queue filled. This is the + /// aggregate of the discontinuity markers in `entries`. + pub lost: u64, +} + +/// Reads the foreground stream, tracking a cursor so each call to +/// [`Self::collect_unseen`] returns only entries recorded since the previous +/// call. Independent collectors do not affect each other. +pub struct ForegroundJournalCollector { + cursor: u64, + ring: Arc, +} + +impl ForegroundJournalCollector { + /// Returns entries recorded since the previous call (or since the + /// collector was created), reporting how many were unavailable before this + /// drain observed them. + pub fn collect_unseen(&mut self) -> DrainedEntries { + let end = self.ring.finalized.load(Ordering::Acquire); + if self.cursor >= end { + return DrainedEntries::default(); + } + let retained_start = end.saturating_sub(self.ring.capacity() as u64); + let mut lost = retained_start.saturating_sub(self.cursor); + self.cursor = self.cursor.max(retained_start); + let mut entries = Vec::with_capacity((end - self.cursor) as usize + usize::from(lost > 0)); + if lost > 0 { + entries.push(ForegroundJournalEntry::Discontinuity { lost }); + } + + while self.cursor < end { + if let Some(entry) = self.ring.read(self.cursor) { + entries.push(entry); + } else { + lost += 1; + match entries.last_mut() { + Some(ForegroundJournalEntry::Discontinuity { lost }) => *lost += 1, + _ => entries.push(ForegroundJournalEntry::Discontinuity { lost: 1 }), + } + } + self.cursor += 1; + } + + DrainedEntries { entries, lost } + } +} + +/// A pure state machine that groups foreground journal entries into +/// [`FrameSnapshot`]s. +/// +/// Feed it drained entries in recording order. Work is carried across calls +/// until an explicit [`IntervalBoundary`] arrives. Idle time before an +/// interval's first event is excluded without relying on an elapsed-time +/// heuristic. +#[derive(Debug)] +pub struct IntervalSealer { + interval_start: Instant, + events: Vec, + small_polls: Vec, + dropped_events: u64, + journal_discontinuous: bool, +} + +impl IntervalSealer { + /// Creates a sealer whose first interval starts at `start` (typically + /// the moment the consumer's collector was created). + pub fn new(start: Instant) -> Self { + Self { + interval_start: start, + events: Vec::new(), + small_polls: Vec::new(), + dropped_events: 0, + journal_discontinuous: false, + } + } + + /// Accounts for a loss supplied outside a collector drain. Collector losses + /// are already represented by ordered discontinuity entries. + pub fn note_lost(&mut self, lost: u64) { + self.dropped_events += lost; + self.journal_discontinuous |= lost > 0; + } + + /// Processes a batch of drained entries, returning the snapshots completed + /// by explicit boundaries. Work without a following boundary is carried + /// over to subsequent calls. + pub fn push_entries( + &mut self, + entries: impl IntoIterator, + ) -> Vec { + let mut snapshots = Vec::new(); + for entry in entries { + match entry { + ForegroundJournalEntry::Event(event) => { + if self.is_empty() { + self.interval_start = self.interval_start.max(event.start_time()); + } + match event { + ForegroundEvent::SmallPolls(flush) => self.push_small_polls(flush), + event => self.push_event(event), + } + } + ForegroundJournalEntry::Boundary(boundary) => { + if let IntervalBoundary::Presented(presented) = boundary { + if self.is_empty() { + self.interval_start = self + .interval_start + .max(presented.presentation.present_start); + } + self.push_event(ForegroundEvent::Present(presented.presentation)); + } + if self.is_empty() { + self.interval_start = self.interval_start.max(boundary.end_time()); + } else { + snapshots.push(self.seal(boundary)); + } + } + // Pending-frame state gates boundaries on the writer side; + // the entries remain in the stream for consumers that want + // dirty timing, but the sealer has no use for them. + ForegroundJournalEntry::FrameState(_) + | ForegroundJournalEntry::PlatformSignal(_) + | ForegroundJournalEntry::ReentrantFrameSkipped { .. } => {} + ForegroundJournalEntry::Discontinuity { lost } => self.note_lost(lost), + } + } + snapshots + } + + fn is_empty(&self) -> bool { + self.events.is_empty() + && self.small_polls.is_empty() + && self.dropped_events == 0 + && !self.journal_discontinuous + } + + fn push_event(&mut self, event: ForegroundEvent) { + if self.events.len() >= MAX_INTERVAL_EVENTS { + self.dropped_events += 1; + } else { + self.events.push(event); + } + } + + fn push_small_polls(&mut self, flush: SmallPollFlush) { + if self.small_polls.len() >= MAX_INTERVAL_EVENTS + && let Some(last) = self.small_polls.last_mut() + { + // Degrade gracefully at the cap: widen the last flush's span + // instead of dropping poll time, at the cost of coarser + // apportioning. + last.summary.add(flush.summary); + last.since = last.since.min(flush.since); + last.until = last.until.max(flush.until); + } else { + self.small_polls.push(flush); + } + } + + fn seal(&mut self, boundary: IntervalBoundary) -> FrameSnapshot { + let ended = boundary.end_time(); + let snapshot = FrameSnapshot { + interval_start: self.interval_start, + boundary, + events: std::mem::take(&mut self.events), + small_polls: std::mem::take(&mut self.small_polls), + dropped_events: std::mem::take(&mut self.dropped_events), + journal_discontinuous: std::mem::take(&mut self.journal_discontinuous), + }; + self.interval_start = ended; + snapshot + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use scheduler::SpawnTime; + + use super::*; + use crate::{WindowId, profiler::YieldTime}; + + #[test] + fn platform_signals_are_journaled_without_sealing_or_occupying_time() { + let (journal, _guard) = install_test_foreground_journal(32, 4); + let mut collector = journal.collector(); + let start = Instant::now(); + let mut sealer = IntervalSealer::new(start); + let window_id = WindowId::from(1); + for source in [ + crate::FrameRequestSource::NativeCallback, + crate::FrameRequestSource::LocalSchedule, + ] { + for signal_at in [None, Some(start + Duration::from_secs(5))] { + record_platform_signal(PlatformSignal { + window_id, + signal_at, + handled_at: start + Duration::from_millis(5), + source, + }); + let entries = collector.collect_unseen().entries; + let [ForegroundJournalEntry::PlatformSignal(signal)] = entries.as_slice() else { + panic!("expected a raw platform signal, got {entries:?}"); + }; + assert_eq!(signal.window_id, window_id); + assert_eq!(signal.signal_at, signal_at); + assert_eq!(signal.handled_at, start + Duration::from_millis(5)); + assert_eq!(signal.source, source); + assert!(sealer.push_entries(entries).is_empty()); + } + } + let ended_at = start + Duration::from_millis(10); + // An enclosing work span may complete after the callback metadata. + let snapshots = sealer.push_entries([ + input_entry(start, ended_at), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }), + ]); + let [snapshot] = snapshots.as_slice() else { + panic!("expected one snapshot, got {snapshots:?}"); + }; + assert_eq!(snapshot.interval_start, start); + assert_eq!(snapshot.interval_end(), ended_at); + assert_eq!(snapshot.occupancy(), Duration::from_millis(10)); + assert_eq!(snapshot.events.len(), 1); + } + + #[test] + fn completed_skips_seal_without_retiring_pending_demand() { + let start = Instant::now(); + let at = start + Duration::from_millis(10); + let window_id = WindowId::from(1); + for reason in [ + FrameSkipReason::NoRenderNeeded, + FrameSkipReason::InactiveFrameRateLimit, + FrameSkipReason::ThermalFrameRateLimit, + ] { + let (mut writer, mut collector) = test_journal(ForegroundRunnableCounter::new()); + writer.begin_turn(); + writer.record_frame_pending(window_id, start); + writer.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end: start + Duration::from_millis(2), + caused_invalidation: false, + })); + writer.record_frame_skipped(FrameSkipped { + window_id, + at, + reason, + }); + + let mut sealer = IntervalSealer::new(start); + let snapshots = sealer.push_entries(collector.collect_unseen().entries); + let [snapshot] = snapshots.as_slice() else { + panic!("expected a completed skip, got {snapshots:?}"); + }; + assert!( + matches!(snapshot.boundary, IntervalBoundary::FrameSkipped(skipped) + if skipped.window_id == window_id && skipped.at == at && skipped.reason == reason) + ); + assert_eq!(snapshot.interval_end(), at); + assert_eq!(snapshot.boundary.dirty_at(), None); + assert_eq!(snapshot.occupancy(), Duration::from_millis(2)); + + writer.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start: at, + end: at + Duration::from_millis(1), + caused_invalidation: false, + })); + writer.end_turn(at + Duration::from_millis(1)); + assert!( + !collector + .collect_unseen() + .entries + .iter() + .any(|entry| matches!(entry, ForegroundJournalEntry::Boundary(_))), + "pending demand must still block idle boundaries" + ); + } + } + + /// Platforms that request a frame every refresh skip most of them while + /// idle. Those skips seal nothing and leave folded polls folded, so + /// end-of-turn idle handling can still discard folded-only work. + #[test] + fn idle_refreshes_seal_nothing_and_keep_polls_folded() { + let start = Instant::now(); + let window_id = WindowId::from(1); + let (mut writer, mut collector) = test_journal(ForegroundRunnableCounter::new()); + for refresh in 0..3u32 { + let at = start + Duration::from_millis(16) * refresh; + writer.begin_turn(); + writer.fold_small_poll(task_timing(at, at + Duration::from_micros(50))); + writer.record_platform_signal(PlatformSignal { + window_id, + signal_at: Some(at), + handled_at: at, + source: crate::FrameRequestSource::NativeCallback, + }); + writer.record_frame_skipped(FrameSkipped { + window_id, + at, + reason: FrameSkipReason::NoRenderNeeded, + }); + writer.end_turn(at + Duration::from_micros(60)); + } + + let entries = collector.collect_unseen().entries; + assert!( + entries + .iter() + .all(|entry| matches!(entry, ForegroundJournalEntry::PlatformSignal(_))), + "expected only platform signals, got {entries:?}" + ); + assert!(IntervalSealer::new(start).push_entries(entries).is_empty()); + } + + #[test] + fn reentrant_skip_does_not_seal_the_enclosing_draw() { + let (journal, _guard) = install_test_foreground_journal(32, 4); + let mut collector = journal.collector(); + let start = Instant::now(); + let window_id = WindowId::from(1); + let mut sealer = IntervalSealer::new(start); + record_platform_signal(PlatformSignal { + window_id, + signal_at: None, + handled_at: start + Duration::from_millis(5), + source: crate::FrameRequestSource::LocalSchedule, + }); + record_reentrant_frame_skipped(window_id, start + Duration::from_millis(6)); + let entries = collector.collect_unseen().entries; + assert!(matches!( + entries.as_slice(), + [ + ForegroundJournalEntry::PlatformSignal(_), + ForegroundJournalEntry::ReentrantFrameSkipped { + window_id: skipped_window_id, + at, + } + ] + if *skipped_window_id == window_id && *at == start + Duration::from_millis(6) + )); + assert!(sealer.push_entries(entries).is_empty()); + + let frame = FrameTiming { + window_id, + dirty_at: Some(start), + invalidations: 1, + draw_start: start, + draw_end: start + Duration::from_millis(20), + }; + record_draw(frame); + record_present(presentation_timing(window_id, frame.draw_end), Some(frame)); + let snapshots = sealer.push_entries(collector.collect_unseen().entries); + let [snapshot] = snapshots.as_slice() else { + panic!("expected the enclosing presentation, got {snapshots:?}"); + }; + assert!(matches!(snapshot.boundary, IntervalBoundary::Presented(_))); + assert_eq!(snapshot.occupancy(), Duration::from_millis(20)); + } + + #[gpui::test] + fn wake_rechecks_native_visibility(cx: &mut crate::TestAppContext) { + use crate::PlatformWindow as _; + let handle = cx.add_window(|_, _| crate::Empty); + let platform = cx.test_window(handle.into()); + platform.on_visibility_change(Box::new(|_| {})); + platform.simulate_visibility_change(WindowVisibility::Hidden); + handle + .update(cx, |_, window, _| assert!(window.is_visible())) + .expect("window"); + cx.update(|cx| { + cx.system_wake_observers + .clone() + .retain(&(), |callback| callback(cx)) + }); + cx.run_until_parked(); + handle + .update(cx, |_, window, _| assert!(!window.is_visible())) + .expect("window"); + } + + #[test] + fn power_interruption_drops_the_poll_before_small_poll_folding() { + let (journal, _guard) = install_test_foreground_journal(32, 4); + let mut collector = journal.collector(); + let start = Instant::now(); + begin_foreground_turn(); + record_power_transition(PowerState::Suspended); + record_power_transition(PowerState::Awake); + assert!(power_interrupted_since(start)); + record_task_poll(task_timing(start, start + Duration::from_micros(10))); + assert!( + collector + .collect_unseen() + .entries + .iter() + .all(|entry| matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::PowerTransition { .. }) + )) + ); + with_journal(|writer| assert!(writer.small_polls.is_none())); + begin_foreground_turn(); + let start = Instant::now(); + assert!(!power_interrupted_since(start)); + record_task_poll(task_timing(start, start + TASK_POLL_FLOOR)); + assert!( + collector + .collect_unseen() + .entries + .iter() + .any(|entry| matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(_)) + )) + ); + } + + #[test] + fn a_poll_spanning_an_unnotified_suspend_is_dropped() { + let (journal, _guard) = install_test_foreground_journal(32, 4); + let mut collector = journal.collector(); + let window = WindowId::from(1); + record_frame_pending(window, Instant::now()); + begin_foreground_turn(); + let start = Instant::now(); + advance_test_suspended_time(Duration::from_secs(2)); + record_task_poll(task_timing(start, Instant::now())); + let entries = collector.collect_unseen().entries; + assert!(entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::PowerTransition { .. }) + ))); + assert!(!entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(_)) + ))); + with_journal(|writer| assert_eq!(writer.windows[&window].dirty_at, None)); + + begin_foreground_turn(); + let start = Instant::now(); + record_task_poll(task_timing(start, start + TASK_POLL_FLOOR)); + let entries = collector.collect_unseen().entries; + assert!(entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(_)) + ))); + assert!(!entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::PowerTransition { .. }) + ))); + } + + #[test] + fn suspend_clock_ignores_jitter_and_notified_suspends() { + let (journal, _guard) = install_test_foreground_journal(32, 4); + let mut collector = journal.collector(); + record_power_transition(PowerState::Suspended); + advance_test_suspended_time(Duration::from_secs(60)); + record_power_transition(PowerState::Awake); + advance_test_suspended_time(MIN_DETECTED_SUSPEND - Duration::from_millis(1)); + begin_foreground_turn(); + let start = Instant::now(); + record_task_poll(task_timing(start, start + TASK_POLL_FLOOR)); + let entries = collector.collect_unseen().entries; + let power_transitions = entries + .iter() + .filter(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::PowerTransition { .. }) + ) + }) + .count(); + assert_eq!(power_transitions, 2, "only the two notifications"); + assert!(entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(_)) + ))); + } + + #[test] + fn visibility_and_power_changes_clear_pending_frames() { + let (mut writer, _) = test_journal(ForegroundRunnableCounter::new()); + let start = Instant::now(); + let first = WindowId::from(1); + let second = WindowId::from(2); + writer.set_visibility(first, WindowVisibility::Hidden, start); + writer.record_frame_pending(first, start); + assert!(!writer.has_unexpired_pending_frame(start)); + writer.record_frame_pending(second, start); + assert!(writer.has_unexpired_pending_frame(start)); + writer.set_visibility(second, WindowVisibility::Hidden, start); + assert!(!writer.has_unexpired_pending_frame(start)); + writer.set_visibility(first, WindowVisibility::Visible, start); + writer.record_frame_pending(first, start); + assert!(writer.has_unexpired_pending_frame(start)); + writer.power_transition(PowerState::Suspended, start); + assert_eq!(writer.windows[&first].dirty_at, None); + writer.record_frame_pending(first, start); + assert!(!writer.has_unexpired_pending_frame(start)); + // A repeated notification is not a transition. + writer.power_transition(PowerState::Suspended, start + FRAME_DEADLINE); + assert_eq!(writer.power_changed_at, Some(start)); + writer.power_transition(PowerState::Awake, start); + writer.record_frame_pending(first, start); + assert!(writer.has_unexpired_pending_frame(start)); + writer.record_present(presentation_timing(first, start + FRAME_DEADLINE), None); + assert_eq!(writer.windows[&first].dirty_at, None); + writer.record_window_closed(first, start); + assert!(!writer.windows.contains_key(&first)); + } + + #[test] + fn draw_waits_for_its_presentation_boundary() { + let start = Instant::now(); + let input = InputTiming { + kind: "test", + start: start + Duration::from_millis(1), + end: start + Duration::from_millis(2), + caused_invalidation: true, + }; + let frame = FrameTiming { + window_id: WindowId::from(1), + dirty_at: Some(input.start), + invalidations: 1, + draw_start: start + Duration::from_millis(3), + draw_end: start + Duration::from_millis(4), + }; + let presentation = PresentTiming { + window_id: frame.window_id, + present_start: start + Duration::from_millis(5), + present_end: start + Duration::from_millis(6), + animation_interval: None, + }; + let mut sealer = IntervalSealer::new(start); + + let snapshots = sealer.push_entries([ + ForegroundJournalEntry::Event(ForegroundEvent::Input(input)), + ForegroundJournalEntry::Event(ForegroundEvent::Draw(frame)), + ]); + assert!(snapshots.is_empty()); + + let snapshots = sealer.push_entries([ForegroundJournalEntry::Boundary( + IntervalBoundary::Presented(PresentedFrame { + frame, + presentation, + }), + )]); + let [snapshot] = snapshots.as_slice() else { + panic!("expected one presentation-sealed snapshot, got {snapshots:?}"); + }; + assert_eq!(snapshot.interval_start, input.start); + assert_eq!(snapshot.interval_end(), presentation.present_end); + assert_eq!(snapshot.events.len(), 3); + assert!(matches!( + snapshot.events.last(), + Some(ForegroundEvent::Present(timing)) + if timing.present_duration() == Duration::from_millis(1) + )); + assert_eq!( + match snapshot.boundary { + IntervalBoundary::Presented(presented) => { + presented.dirty_to_present_duration() + } + IntervalBoundary::FrameSkipped(_) + | IntervalBoundary::Idle { .. } + | IntervalBoundary::PowerTransition { .. } => None, + }, + Some(Duration::from_millis(5)) + ); + } + + #[test] + fn outermost_turn_seals_no_frame_work_when_idle() { + let start = Instant::now(); + let counter = ForegroundRunnableCounter::new(); + let (mut journal, mut collector) = test_journal(counter); + let events = [ + ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end: start + Duration::from_millis(1), + caused_invalidation: false, + }), + ForegroundEvent::Action(ActionTiming { + name: "test.action", + start: start + Duration::from_millis(2), + end: start + Duration::from_millis(3), + }), + ForegroundEvent::TaskPoll(task_timing( + start + Duration::from_millis(4), + start + Duration::from_millis(5), + )), + ]; + + for event in events { + journal.begin_turn(); + journal.record_event(event); + journal.end_turn(event.end_time()); + } + + let entries = collector.collect_unseen().entries; + for event in events { + assert!(entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) + if *ended_at == event.end_time() + ) + })); + } + } + + #[test] + fn nested_turns_only_seal_after_the_outermost_turn() { + let start = Instant::now(); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + let action = ForegroundEvent::Action(ActionTiming { + name: "test.action", + start, + end: start + Duration::from_millis(1), + }); + let input = ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end: start + Duration::from_millis(2), + caused_invalidation: false, + }); + + journal.begin_turn(); + journal.begin_turn(); + journal.record_event(action); + journal.end_turn(action.end_time()); + assert!(!has_boundary_at( + &collector.collect_unseen().entries, + action.end_time() + )); + + journal.record_event(input); + journal.end_turn(input.end_time()); + assert!(collector.collect_unseen().entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) + if *ended_at == input.end_time() + ) + })); + } + + #[test] + fn an_immediately_ready_runnable_prevents_an_idle_boundary_between_polls() { + let start = Instant::now(); + let counter = ForegroundRunnableCounter::new(); + let (mut journal, mut collector) = test_journal(counter.clone()); + counter.queued(); + counter.queued(); + + let first = ForegroundEvent::TaskPoll(task_timing(start, start + Duration::from_millis(1))); + journal.begin_turn(); + journal.record_event(first); + counter.finished(); + journal.end_turn(first.end_time()); + assert!(!has_boundary_at( + &collector.collect_unseen().entries, + first.end_time() + )); + + let second = ForegroundEvent::TaskPoll(task_timing( + start + Duration::from_millis(2), + start + Duration::from_millis(3), + )); + journal.begin_turn(); + journal.record_event(second); + counter.finished(); + journal.end_turn(second.end_time()); + assert!(collector.collect_unseen().entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) + if *ended_at == second.end_time() + ) + })); + } + + #[test] + fn a_pending_frame_prevents_idle_until_presentation() { + let start = Instant::now(); + let window_id = WindowId::from(0xD17A); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.record_frame_pending(window_id, start); + journal.begin_turn(); + journal.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end: start + Duration::from_millis(1), + caused_invalidation: true, + })); + let input_end = start + Duration::from_millis(1); + journal.end_turn(input_end); + assert!(!has_boundary_at( + &collector.collect_unseen().entries, + input_end + )); + + let frame = frame_timing(window_id, start, start + Duration::from_millis(2)); + let presentation = presentation_timing(window_id, start + Duration::from_millis(3)); + journal.record_present(presentation, Some(frame)); + assert!(collector.collect_unseen().entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) + if presented.frame.window_id == window_id + ) + })); + } + + #[test] + fn a_present_without_a_draw_clears_the_window_pending_state() { + let start = Instant::now(); + let presented_at = start + Duration::from_millis(1); + let window_id = WindowId::from(0xD17B); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.record_frame_pending(window_id, start); + journal.begin_turn(); + journal.record_present(presentation_timing(window_id, presented_at), None); + journal.end_turn(presented_at); + + let entries = collector.collect_unseen().entries; + assert!(entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::Present(timing)) + if timing.window_id == window_id + ) + })); + assert!(has_boundary_at(&entries, presented_at)); + } + + /// A frame that outlives [`FRAME_DEADLINE`] no longer seals an interval + /// of its own: the work that starved it and its eventual presentation + /// stay in one interval, preserving the dirty-to-present association. + #[test] + fn a_presentation_after_the_deadline_seals_the_whole_interval() { + let start = Instant::now(); + let window_id = WindowId::from(0xDEA1); + let presented_at = start + FRAME_DEADLINE + Duration::from_millis(250); + let mut sealer = IntervalSealer::new(start); + let snapshots = sealer.push_entries([ + pending_frame(window_id, start), + input_entry(start, start + Duration::from_millis(20)), + ForegroundJournalEntry::Boundary(presented_boundary(window_id, start, presented_at)), + ]); + let [snapshot] = snapshots.as_slice() else { + panic!("expected one presented snapshot, got {snapshots:?}"); + }; + assert!(matches!( + snapshot.boundary, + IntervalBoundary::Presented(presented) + if presented.frame.window_id == window_id + && presented.dirty_to_present_duration() + == Some(presented_at.duration_since(start)) + )); + assert_eq!(snapshot.interval_end(), presented_at); + assert_eq!(snapshot.events.len(), 2); + } + + /// Frame expiry unblocks idle boundaries on the writer side: retained + /// work after the deadline seals at its turn's end even though the window + /// never presented. + #[test] + fn an_expired_frame_no_longer_blocks_idle_boundaries() { + let start = Instant::now(); + let window_id = WindowId::from(0xDEA2); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.record_frame_pending(window_id, start); + + let blocked_end = start + Duration::from_millis(20); + journal.begin_turn(); + journal.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end: blocked_end, + caused_invalidation: true, + })); + journal.end_turn(blocked_end); + assert!(!has_boundary_at( + &collector.collect_unseen().entries, + blocked_end + )); + + let unblocked_end = start + FRAME_DEADLINE + Duration::from_millis(1); + journal.begin_turn(); + journal.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start: start + FRAME_DEADLINE, + end: unblocked_end, + caused_invalidation: false, + })); + journal.end_turn(unblocked_end); + assert!(has_boundary_at( + &collector.collect_unseen().entries, + unblocked_end + )); + } + + /// Closing a window clears its pending frame, so idle boundaries resume + /// without waiting for the deadline. + #[test] + fn closing_a_window_unblocks_idle_boundaries() { + let start = Instant::now(); + let window_id = WindowId::from(0xDEA7); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.record_frame_pending(window_id, start); + journal.record_window_closed(window_id, start + Duration::from_millis(1)); + + let event_end = start + Duration::from_millis(2); + journal.begin_turn(); + journal.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start: start + Duration::from_millis(1), + end: event_end, + caused_invalidation: false, + })); + journal.end_turn(event_end); + assert!(has_boundary_at( + &collector.collect_unseen().entries, + event_end + )); + } + + /// One window presenting must not unblock idle boundaries while another + /// window's unexpired frame is still pending. + #[test] + fn presenting_one_window_does_not_clear_another_pending_window() { + let start = Instant::now(); + let first_window = WindowId::from(0xDEA5); + let second_window = WindowId::from(0xDEA6); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.record_frame_pending(first_window, start); + journal.record_frame_pending(second_window, start + Duration::from_millis(100)); + journal.record_present( + presentation_timing(first_window, start + Duration::from_millis(500)), + Some(frame_timing( + first_window, + start, + start + Duration::from_millis(500), + )), + ); + + let event_end = start + Duration::from_millis(620); + journal.begin_turn(); + journal.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start: start + Duration::from_millis(600), + end: event_end, + caused_invalidation: false, + })); + journal.end_turn(event_end); + + let entries = collector.collect_unseen().entries; + assert!(entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) + if presented.frame.window_id == first_window + ) + })); + assert!(!has_boundary_at(&entries, event_end)); + } + + #[test] + fn small_polls_are_flushed_immediately_before_a_retained_event() { + let start = Instant::now(); + let first = task_timing( + start + Duration::from_millis(1), + start + Duration::from_millis(1) + Duration::from_micros(20), + ); + let second = task_timing( + start + Duration::from_millis(3), + start + Duration::from_millis(3) + Duration::from_micros(30), + ); + let input = InputTiming { + kind: "test", + start: start + Duration::from_millis(5), + end: start + Duration::from_millis(6), + caused_invalidation: false, + }; + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + + journal.fold_small_poll(first); + journal.fold_small_poll(second); + journal.record_event(ForegroundEvent::Input(input)); + + let drained = collector.collect_unseen(); + let input_index = drained + .entries + .iter() + .position(|entry| { + matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) + if timing.start == input.start + ) + }) + .expect("input event should be retained"); + let Some(ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush))) = input_index + .checked_sub(1) + .and_then(|index| drained.entries.get(index)) + else { + panic!("small-poll summary should immediately precede the input event"); + }; + assert_eq!(flush.summary.count, 2); + assert_eq!(flush.summary.total, Duration::from_micros(50)); + assert_eq!(flush.since, first.start); + assert_eq!(flush.until, second.end.0); + } + + #[test] + fn small_polls_are_flushed_before_frame_state_changes() { + let start = Instant::now(); + let window_id = WindowId::from(0xDEA9); + let poll = task_timing(start, start + Duration::from_micros(50)); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + journal.fold_small_poll(poll); + journal.record_frame_pending(window_id, poll.end.0); + + let entries = collector.collect_unseen().entries; + let pending_index = entries + .iter() + .position(|entry| { + matches!( + entry, + ForegroundJournalEntry::FrameState(FrameStateChange::Pending { + window_id: pending_window, + .. + }) if *pending_window == window_id + ) + }) + .expect("pending frame should be retained"); + assert!(matches!( + pending_index + .checked_sub(1) + .and_then(|index| entries.get(index)), + Some(ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush))) + if flush.since == poll.start && flush.until == poll.end.0 + )); + } + + /// Sporadic wake-ups (a tiny folded poll after which the foreground goes + /// idle, repeatedly) must write nothing to the ring and must not accumulate + /// toward a later retained interval. + #[test] + fn sparse_small_polls_are_discarded_when_the_foreground_returns_to_idle() { + let start = Instant::now(); + let (mut journal, mut collector) = test_journal(ForegroundRunnableCounter::new()); + let mut tiny_wake = |ended_at: Instant| { + journal.begin_turn(); + journal.fold_small_poll(task_timing(ended_at - Duration::from_micros(50), ended_at)); + journal.end_turn(ended_at); + }; + + for second in 1..=160 { + tiny_wake(start + Duration::from_secs(second)); + } + let quiet = collector.collect_unseen().entries; + assert!(quiet.is_empty()); + + // A later retained event seals normally without inheriting the earlier + // folded-only wake-ups. + let retained = ForegroundEvent::TaskPoll(task_timing( + start + Duration::from_secs(161), + start + Duration::from_secs(161) + Duration::from_millis(1), + )); + journal.begin_turn(); + journal.record_event(retained); + journal.end_turn(retained.end_time()); + let entries = collector.collect_unseen().entries; + assert!(has_boundary_at(&entries, retained.end_time())); + assert!(!entries.iter().any(|entry| matches!( + entry, + ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(_)) + ))); + } + + #[test] + fn note_lost_is_reported_on_the_next_snapshot_only() { + let start = Instant::now(); + let mut sealer = IntervalSealer::new(start); + sealer.note_lost(7); + + let snapshots = sealer.push_entries([ + input_entry(start, start + Duration::from_millis(1)), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { + ended_at: start + Duration::from_millis(1), + }), + input_entry( + start + Duration::from_millis(2), + start + Duration::from_millis(3), + ), + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { + ended_at: start + Duration::from_millis(3), + }), + ]); + + assert_eq!(snapshots.len(), 2); + assert_eq!(snapshots[0].dropped_events, 7); + assert_eq!(snapshots[1].dropped_events, 0); + } + + /// Ring losses must be able to surface even when every retained entry was + /// among the losses: a loss-only interval still seals at the next + /// boundary rather than sliding away as empty. + #[test] + fn losses_alone_seal_a_snapshot_at_the_next_boundary() { + let start = Instant::now(); + let mut sealer = IntervalSealer::new(start); + sealer.note_lost(3); + + let snapshots = + sealer.push_entries([ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { + ended_at: start + Duration::from_millis(1), + })]); + + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0].dropped_events, 3); + assert!(snapshots[0].events.is_empty()); + } + + #[test] + fn interval_event_cap_counts_overflowing_events() { + let start = Instant::now(); + let mut sealer = IntervalSealer::new(start); + let overflow = 5; + let mut entries: Vec = (0..(MAX_INTERVAL_EVENTS + overflow) as u64) + .map(|i| { + input_entry( + start + Duration::from_micros(i), + start + Duration::from_micros(i + 1), + ) + }) + .collect(); + entries.push(ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { + ended_at: start + Duration::from_micros((MAX_INTERVAL_EVENTS + overflow) as u64 + 1), + })); + + let snapshots = sealer.push_entries(entries); + + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0].events.len(), MAX_INTERVAL_EVENTS); + assert_eq!(snapshots[0].dropped_events, overflow as u64); + } + + #[test] + fn collect_unseen_returns_each_entry_once_in_order() { + let start = Instant::now(); + let (journal, mut publisher) = ForegroundJournal::new(32, 4); + let mut collector = journal.collector(); + let timestamps: Vec = (0..19) + .map(|i| start + Duration::from_micros(i as u64 + 1)) + .collect(); + publisher.publish(timestamps.iter().map(|&at| input_entry(at, at))); + + let ours = |entries: &[ForegroundJournalEntry]| -> Vec { + entries + .iter() + .filter_map(|entry| match entry { + ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) + if timestamps.contains(&timing.end) => + { + Some(timing.end) + } + _ => None, + }) + .collect() + }; + + let drained = collector.collect_unseen(); + assert_eq!(ours(&drained.entries), timestamps); + + // The cursor advanced past everything: a second drain sees none of + // our entries again. + let drained = collector.collect_unseen(); + assert!(ours(&drained.entries).is_empty()); + } + + #[test] + fn concurrent_collection_preserves_the_complete_logical_sequence() { + const ENTRY_COUNT: u64 = 20_000; + + let origin = Instant::now(); + let (journal, mut publisher) = ForegroundJournal::new(8, 4); + let mut collector = journal.collector(); + let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let collector_done = Arc::clone(&done); + let collector_task = std::thread::spawn(move || { + let mut observed = Vec::new(); + loop { + let drained = collector.collect_unseen(); + observed.extend(normalize_ring_drain(origin, drained.entries)); + if collector_done.load(Ordering::Acquire) { + let drained = collector.collect_unseen(); + observed.extend(normalize_ring_drain(origin, drained.entries)); + return (collector, observed); + } + std::thread::yield_now(); + } + }); + + for sequence in 0..ENTRY_COUNT { + publisher.publish([ring_entry(origin, sequence)]); + } + done.store(true, Ordering::Release); + + let (mut collector, mut observed) = collector_task + .join() + .expect("collector thread should not panic"); + publisher.flush_pending(); + observed.extend(normalize_ring_drain( + origin, + collector.collect_unseen().entries, + )); + + let mut expected_sequence = 0; + for entry in observed { + match entry { + ModelDrainEntry::Entry(sequence) => { + assert_eq!(sequence, expected_sequence); + expected_sequence += 1; + } + ModelDrainEntry::Discontinuity(lost) => expected_sequence += lost, + } + } + assert_eq!(expected_sequence, ENTRY_COUNT); + assert_eq!( + publisher.ring.finalized.load(Ordering::Acquire), + ENTRY_COUNT + ); + } + + #[derive(Clone, Debug)] + enum RingOperation { + Publish, + Collect(u8), + NewCollector, + Pin(u8), + Unpin(u8), + } + + #[derive(Clone, Debug, PartialEq, Eq)] + enum ModelDrainEntry { + Entry(u64), + Discontinuity(u64), + } + + struct ModelRing { + capacity: usize, + pending_capacity: usize, + next_sequence: u64, + finalized: u64, + offered: u64, + slots: Vec>, + pinned: Vec, + pending: VecDeque<(u64, u64)>, + dropped_after_pending: u64, + } + + impl ModelRing { + fn new(capacity: usize, pending_capacity: usize) -> Self { + Self { + capacity, + pending_capacity, + next_sequence: 0, + finalized: 0, + offered: 0, + slots: vec![None; capacity], + pinned: vec![false; capacity], + pending: VecDeque::new(), + dropped_after_pending: 0, + } + } + + fn publish(&mut self) { + self.flush_pending(); + let sequence = self.next_sequence; + self.next_sequence += 1; + self.offered = self.next_sequence; + + if !self.pending.is_empty() || self.dropped_after_pending > 0 { + if self.dropped_after_pending == 0 && self.pending.len() < self.pending_capacity { + self.pending.push_back((sequence, sequence)); + } else { + self.dropped_after_pending += 1; + } + } else { + let index = sequence as usize % self.capacity; + if self.pinned[index] { + if self.pending_capacity > 0 { + self.pending.push_back((sequence, sequence)); + } else { + self.dropped_after_pending = 1; + } + } else { + self.slots[index] = Some((sequence, sequence)); + self.finalized = sequence + 1; + } + } + self.flush_pending(); + } + + fn flush_pending(&mut self) { + while let Some(&(sequence, value)) = self.pending.front() { + let index = sequence as usize % self.capacity; + if self.pinned[index] { + return; + } + self.slots[index] = Some((sequence, value)); + self.pending.pop_front(); + self.finalized = sequence + 1; + } + + if self.dropped_after_pending > 0 { + self.finalized = self.next_sequence; + self.dropped_after_pending = 0; + } + } + + fn collect(&self, cursor: &mut u64) -> (Vec, u64) { + let end = self.finalized; + if *cursor >= end { + return (Vec::new(), 0); + } + + let retained_start = end.saturating_sub(self.capacity as u64); + let mut lost = retained_start.saturating_sub(*cursor); + *cursor = (*cursor).max(retained_start); + let mut entries = Vec::new(); + if lost > 0 { + entries.push(ModelDrainEntry::Discontinuity(lost)); + } + while *cursor < end { + let index = *cursor as usize % self.capacity; + match self.slots[index] { + Some((sequence, value)) if sequence == *cursor => { + entries.push(ModelDrainEntry::Entry(value)); + } + _ => { + lost += 1; + match entries.last_mut() { + Some(ModelDrainEntry::Discontinuity(lost)) => *lost += 1, + _ => entries.push(ModelDrainEntry::Discontinuity(1)), + } + } + } + *cursor += 1; + } + (entries, lost) + } + } + + fn ring_entry(origin: Instant, sequence: u64) -> ForegroundJournalEntry { + let at = origin + Duration::from_micros(sequence); + input_entry(at, at) + } + + fn normalize_ring_drain( + origin: Instant, + entries: Vec, + ) -> Vec { + entries + .into_iter() + .map(|entry| match entry { + ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) => { + ModelDrainEntry::Entry(timing.start.duration_since(origin).as_micros() as u64) + } + ForegroundJournalEntry::Discontinuity { lost } => { + ModelDrainEntry::Discontinuity(lost) + } + other => panic!("ring property observed an unexpected entry: {other:?}"), + }) + .collect() + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct NormalizedEvent { + kind: u8, + start: u64, + end: u64, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct NormalizedSmallPolls { + count: u64, + total_micros: u64, + since: u64, + until: u64, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct NormalizedSnapshot { + interval_start: u64, + boundary_kind: u8, + interval_end: u64, + events: Vec, + small_polls: Vec, + dropped_events: u64, + journal_discontinuous: bool, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + struct NormalizedSealerTail { + interval_start: u64, + events: Vec, + small_polls: Vec, + dropped_events: u64, + journal_discontinuous: bool, + } + + fn completion_order_entries( + origin: Instant, + specifications: &[(u8, u8, u8)], + ) -> Vec { + let mut completed_at_micros = 1u64; + specifications + .iter() + .map(|(advance, span, kind)| { + completed_at_micros += u64::from(*advance) + 1; + let started_at_micros = completed_at_micros.saturating_sub(u64::from(*span)); + let start = origin + Duration::from_micros(started_at_micros); + let end = origin + Duration::from_micros(completed_at_micros); + match kind % 7 { + 0 => input_entry(start, end), + 1 => ForegroundJournalEntry::Event(ForegroundEvent::Action(ActionTiming { + name: "test.action", + start, + end, + })), + 2 => ForegroundJournalEntry::Event(ForegroundEvent::TaskPoll(task_timing( + start, end, + ))), + 3 => { + ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(SmallPollFlush { + summary: PollSummary { + count: u64::from(*span % 5) + 1, + total: Duration::from_micros(u64::from(*span)), + }, + since: start, + until: end, + })) + } + 4 => pending_frame(WindowId::from(completed_at_micros), start), + 5 => ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at: end }), + _ => ForegroundJournalEntry::Boundary(presented_boundary( + WindowId::from(completed_at_micros), + start, + end, + )), + } + }) + .collect() + } + + fn normalize_event(origin: Instant, event: ForegroundEvent) -> NormalizedEvent { + let kind = match event { + ForegroundEvent::TaskPoll(_) => 0, + ForegroundEvent::Action(_) => 1, + ForegroundEvent::Input(_) => 2, + ForegroundEvent::Draw(_) => 3, + ForegroundEvent::Present(_) => 4, + ForegroundEvent::SmallPolls(_) => 5, + }; + NormalizedEvent { + kind, + start: event.start_time().duration_since(origin).as_micros() as u64, + end: event.end_time().duration_since(origin).as_micros() as u64, + } + } + + fn normalize_small_polls(origin: Instant, flush: SmallPollFlush) -> NormalizedSmallPolls { + NormalizedSmallPolls { + count: flush.summary.count, + total_micros: flush.summary.total.as_micros() as u64, + since: flush.since.duration_since(origin).as_micros() as u64, + until: flush.until.duration_since(origin).as_micros() as u64, + } + } + + fn normalize_snapshot(origin: Instant, snapshot: &FrameSnapshot) -> NormalizedSnapshot { + NormalizedSnapshot { + interval_start: snapshot.interval_start.duration_since(origin).as_micros() as u64, + boundary_kind: match snapshot.boundary { + IntervalBoundary::Idle { .. } => 0, + IntervalBoundary::Presented(_) => 1, + IntervalBoundary::PowerTransition { .. } => 2, + IntervalBoundary::FrameSkipped(_) => 3, + }, + interval_end: snapshot.interval_end().duration_since(origin).as_micros() as u64, + events: snapshot + .events + .iter() + .copied() + .map(|event| normalize_event(origin, event)) + .collect(), + small_polls: snapshot + .small_polls + .iter() + .copied() + .map(|flush| normalize_small_polls(origin, flush)) + .collect(), + dropped_events: snapshot.dropped_events, + journal_discontinuous: snapshot.journal_discontinuous, + } + } + + fn normalize_tail(origin: Instant, sealer: &IntervalSealer) -> NormalizedSealerTail { + NormalizedSealerTail { + interval_start: sealer.interval_start.duration_since(origin).as_micros() as u64, + events: sealer + .events + .iter() + .copied() + .map(|event| normalize_event(origin, event)) + .collect(), + small_polls: sealer + .small_polls + .iter() + .copied() + .map(|flush| normalize_small_polls(origin, flush)) + .collect(), + dropped_events: sealer.dropped_events, + journal_discontinuous: sealer.journal_discontinuous, + } + } + + fn reference_seal( + origin: Instant, + entries: &[ForegroundJournalEntry], + ) -> (Vec, NormalizedSealerTail) { + let mut interval_start = 0; + let mut events = Vec::new(); + let mut small_polls = Vec::new(); + let mut snapshots = Vec::new(); + let mut dropped_events = 0; + let mut journal_discontinuous = false; + + for entry in entries { + match *entry { + ForegroundJournalEntry::Event(event) => { + let event_start = event.start_time().duration_since(origin).as_micros() as u64; + if events.is_empty() + && small_polls.is_empty() + && dropped_events == 0 + && !journal_discontinuous + { + interval_start = interval_start.max(event_start); + } + match event { + ForegroundEvent::SmallPolls(flush) => { + small_polls.push(normalize_small_polls(origin, flush)); + } + event => events.push(normalize_event(origin, event)), + } + } + ForegroundJournalEntry::Boundary(boundary) => { + if let IntervalBoundary::Presented(presented) = boundary { + let present = ForegroundEvent::Present(presented.presentation); + if events.is_empty() + && small_polls.is_empty() + && dropped_events == 0 + && !journal_discontinuous + { + interval_start = + interval_start + .max(present.start_time().duration_since(origin).as_micros() + as u64); + } + events.push(normalize_event(origin, present)); + } + + let interval_end = + boundary.end_time().duration_since(origin).as_micros() as u64; + if events.is_empty() + && small_polls.is_empty() + && dropped_events == 0 + && !journal_discontinuous + { + interval_start = interval_start.max(interval_end); + } else { + snapshots.push(NormalizedSnapshot { + interval_start, + boundary_kind: match boundary { + IntervalBoundary::Idle { .. } => 0, + IntervalBoundary::Presented(_) => 1, + IntervalBoundary::PowerTransition { .. } => 2, + IntervalBoundary::FrameSkipped(_) => 3, + }, + interval_end, + events: std::mem::take(&mut events), + small_polls: std::mem::take(&mut small_polls), + dropped_events: std::mem::take(&mut dropped_events), + journal_discontinuous: std::mem::take(&mut journal_discontinuous), + }); + interval_start = interval_end; + } + } + ForegroundJournalEntry::FrameState(_) + | ForegroundJournalEntry::PlatformSignal(_) + | ForegroundJournalEntry::ReentrantFrameSkipped { .. } => {} + ForegroundJournalEntry::Discontinuity { lost } => { + dropped_events += lost; + journal_discontinuous = true; + } + } + } + + ( + snapshots, + NormalizedSealerTail { + interval_start, + events, + small_polls, + dropped_events, + journal_discontinuous, + }, + ) + } + + fn reference_occupancy_micros( + event_spans: &[(u8, u8)], + small_poll_spans: &[(u8, u8, u8)], + window_start: u8, + window_end: u8, + ) -> u64 { + let mut clamped_spans = event_spans + .iter() + .map(|&(first, second)| { + let start = first.min(second).max(window_start); + let end = first.max(second).min(window_end).max(start); + (start, end) + }) + .collect::>(); + clamped_spans.sort_unstable(); + + let mut occupied = 0u64; + let mut merged_until = None; + for (start, end) in clamped_spans { + let start = merged_until.map_or(start, |until| start.max(until)); + occupied += u64::from(end.saturating_sub(start)); + merged_until = Some(merged_until.map_or(end, |until| until.max(end))); + } + + occupied + + small_poll_spans + .iter() + .map(|&(first, second, factor)| { + let since = first.min(second); + let until = first.max(second); + if since == until { + if since >= window_start && since <= window_end { + u64::from(factor) + } else { + 0 + } + } else { + let overlap_start = since.max(window_start); + let overlap_end = until.min(window_end).max(overlap_start); + u64::from(overlap_end - overlap_start) * u64::from(factor) + } + }) + .sum::() + } + + #[derive(Debug, Clone)] + struct WriterOperation { + kind: u8, + advance: u16, + argument: u8, + } + + #[derive(Debug, Clone, PartialEq, Eq)] + enum WriterEntry { + Input(u64), + SmallPolls { + count: u64, + total_micros: u64, + since: u64, + until: u64, + }, + Pending { + window: u64, + at: u64, + }, + Closed { + window: u64, + at: u64, + }, + Present { + window: u64, + at: u64, + }, + Presented { + window: u64, + at: u64, + }, + Idle(u64), + } + + #[derive(Default)] + struct ModelSmallPolls { + count: u64, + total_micros: u64, + since: u64, + until: u64, + } + + #[derive(Default)] + struct ModelWriter { + turn_depth: usize, + runnables: usize, + pending_frames: HashMap, + retained_since_boundary: bool, + small_polls: Option, + entries: Vec, + } + + impl ModelWriter { + fn record_entry(&mut self, entry: WriterEntry) { + if let Some(small_polls) = self.small_polls.take() { + self.entries.push(WriterEntry::SmallPolls { + count: small_polls.count, + total_micros: small_polls.total_micros, + since: small_polls.since, + until: small_polls.until, + }); + } + let boundary = matches!(entry, WriterEntry::Presented { .. } | WriterEntry::Idle(_)); + self.entries.push(entry); + if boundary { + self.retained_since_boundary = false; + } + } + + fn record_input(&mut self, at: u64) { + self.retained_since_boundary = true; + self.record_entry(WriterEntry::Input(at)); + } + + fn fold_small_poll(&mut self, since: u64, until: u64) { + let small_polls = self.small_polls.get_or_insert(ModelSmallPolls { + since, + until, + ..ModelSmallPolls::default() + }); + small_polls.count += 1; + small_polls.total_micros += until - since; + small_polls.since = small_polls.since.min(since); + small_polls.until = small_polls.until.max(until); + } + + fn record_pending(&mut self, window: u64, at: u64) { + let should_record = self + .pending_frames + .get(&window) + .is_none_or(|previous| at.saturating_sub(*previous) >= 1_000_000); + if should_record { + self.pending_frames.insert(window, at); + self.record_entry(WriterEntry::Pending { window, at }); + } + } + + fn record_closed(&mut self, window: u64, at: u64) { + self.pending_frames.remove(&window); + self.record_entry(WriterEntry::Closed { window, at }); + } + + fn record_present(&mut self, window: u64, at: u64, has_frame: bool) { + if has_frame { + self.pending_frames.remove(&window); + self.record_entry(WriterEntry::Presented { window, at }); + } else { + self.pending_frames.remove(&window); + self.retained_since_boundary = true; + self.record_entry(WriterEntry::Present { window, at }); + } + } + + fn end_turn(&mut self, at: u64) { + if self.turn_depth == 0 { + return; + } + self.turn_depth -= 1; + if self.turn_depth > 0 || self.runnables > 0 { + return; + } + self.pending_frames + .retain(|_, dirty_at| at.saturating_sub(*dirty_at) < 1_000_000); + if !self.pending_frames.is_empty() { + return; + } + if !self.retained_since_boundary { + self.small_polls = None; + return; + } + self.record_entry(WriterEntry::Idle(at)); + } + } + + fn normalize_writer_entries( + origin: Instant, + entries: Vec, + ) -> Vec { + let at = |instant: Instant| instant.duration_since(origin).as_micros() as u64; + entries + .into_iter() + .map(|entry| match entry { + ForegroundJournalEntry::Event(ForegroundEvent::Input(timing)) => { + WriterEntry::Input(at(timing.end)) + } + ForegroundJournalEntry::Event(ForegroundEvent::SmallPolls(flush)) => { + WriterEntry::SmallPolls { + count: flush.summary.count, + total_micros: flush.summary.total.as_micros() as u64, + since: at(flush.since), + until: at(flush.until), + } + } + ForegroundJournalEntry::Event(ForegroundEvent::Present(timing)) => { + WriterEntry::Present { + window: timing.window_id.as_u64(), + at: at(timing.present_end), + } + } + ForegroundJournalEntry::FrameState(FrameStateChange::Pending { + window_id, + dirty_at, + }) => WriterEntry::Pending { + window: window_id.as_u64(), + at: at(dirty_at), + }, + ForegroundJournalEntry::FrameState(FrameStateChange::Closed { + window_id, + at: closed_at, + }) => WriterEntry::Closed { + window: window_id.as_u64(), + at: at(closed_at), + }, + ForegroundJournalEntry::Boundary(IntervalBoundary::Presented(presented)) => { + WriterEntry::Presented { + window: presented.frame.window_id.as_u64(), + at: at(presented.presentation.present_end), + } + } + ForegroundJournalEntry::Boundary(IntervalBoundary::Idle { ended_at }) => { + WriterEntry::Idle(at(ended_at)) + } + other => panic!("writer property emitted an unexpected entry: {other:?}"), + }) + .collect() + } + + proptest! { + #![proptest_config(ProptestConfig { + failure_persistence: None, + ..ProptestConfig::default() + })] + + #[test] + fn ring_matches_reference_model_under_wrap_collisions_and_independent_cursors( + capacity in 1usize..=8, + pending_capacity in 0usize..=4, + operations in prop::collection::vec( + prop_oneof![ + 5 => Just(RingOperation::Publish), + 3 => any::().prop_map(RingOperation::Collect), + 1 => Just(RingOperation::NewCollector), + 2 => any::().prop_map(RingOperation::Pin), + 2 => any::().prop_map(RingOperation::Unpin), + ], + 1..=128, + ), + ) { + let origin = Instant::now(); + let (journal, mut publisher) = ForegroundJournal::new(capacity, pending_capacity); + let mut collectors = vec![journal.collector()]; + let mut model_collectors = vec![0]; + let mut model = ModelRing::new(capacity, pending_capacity); + let mut pins: Vec>> = + (0..capacity).map(|_| None).collect(); + + for operation in operations { + match operation { + RingOperation::Publish => { + let sequence = model.next_sequence; + publisher.publish([ring_entry(origin, sequence)]); + model.publish(); + } + RingOperation::Collect(collector) => { + let index = usize::from(collector) % collectors.len(); + let drained = collectors[index].collect_unseen(); + let (expected_entries, expected_lost) = + model.collect(&mut model_collectors[index]); + let observed_entries = normalize_ring_drain(origin, drained.entries); + prop_assert_eq!(observed_entries, expected_entries); + prop_assert_eq!(drained.lost, expected_lost); + } + RingOperation::NewCollector if collectors.len() < 4 => { + collectors.push(journal.collector()); + model_collectors.push(model.offered); + } + RingOperation::NewCollector => {} + RingOperation::Pin(slot) => { + let index = usize::from(slot) % capacity; + if pins[index].is_none() { + pins[index] = JournalSlotReadGuard::try_new(&journal.ring.slots[index]); + prop_assert!(pins[index].is_some()); + model.pinned[index] = true; + } + } + RingOperation::Unpin(slot) => { + let index = usize::from(slot) % capacity; + pins[index].take(); + model.pinned[index] = false; + } + } + + prop_assert_eq!(publisher.next_sequence, model.next_sequence); + prop_assert_eq!( + publisher.ring.offered.load(Ordering::Acquire), + model.offered + ); + prop_assert_eq!( + publisher.ring.finalized.load(Ordering::Acquire), + model.finalized + ); + prop_assert_eq!( + publisher + .pending + .iter() + .map(|entry| entry.sequence) + .collect::>(), + model + .pending + .iter() + .map(|(sequence, _)| *sequence) + .collect::>() + ); + prop_assert_eq!( + publisher.dropped_after_pending, + model.dropped_after_pending + ); + } + + for pin in &mut pins { + pin.take(); + } + model.pinned.fill(false); + publisher.flush_pending(); + model.flush_pending(); + + for (collector, model_cursor) in collectors.iter_mut().zip(&mut model_collectors) { + let drained = collector.collect_unseen(); + let (expected_entries, expected_lost) = model.collect(model_cursor); + let observed_entries = normalize_ring_drain(origin, drained.entries); + prop_assert_eq!(observed_entries, expected_entries); + prop_assert_eq!(drained.lost, expected_lost); + } + } + + #[test] + fn a_presentation_seals_exactly_once_regardless_of_delay( + present_delay_micros in 1u64..=2_000_000 + ) { + let start = Instant::now(); + let window_id = WindowId::from(0xDEA8); + let presented_at = start + Duration::from_micros(present_delay_micros); + let mut sealer = IntervalSealer::new(start); + let snapshots = sealer.push_entries([ + pending_frame(window_id, start), + input_entry(start, start + Duration::from_micros(1)), + ForegroundJournalEntry::Boundary(presented_boundary( + window_id, + start, + presented_at, + )), + ]); + + prop_assert_eq!(snapshots.len(), 1); + prop_assert!(matches!( + snapshots[0].boundary, + IntervalBoundary::Presented(_) + )); + prop_assert_eq!(snapshots[0].interval_end(), presented_at); + } + + #[test] + fn completion_order_sealer_matches_reference_under_arbitrary_batching( + specifications in prop::collection::vec((0u8..16, 0u8..128, any::()), 1..=96), + batch_sizes in prop::collection::vec(1usize..=12, 1..=24), + ) { + let origin = Instant::now(); + let entries = completion_order_entries(origin, &specifications); + let (expected_snapshots, expected_tail) = reference_seal(origin, &entries); + + let mut one_batch_sealer = IntervalSealer::new(origin); + let one_batch_snapshots = one_batch_sealer + .push_entries(entries.iter().copied()) + .iter() + .map(|snapshot| normalize_snapshot(origin, snapshot)) + .collect::>(); + prop_assert_eq!(&one_batch_snapshots, &expected_snapshots); + prop_assert_eq!(&normalize_tail(origin, &one_batch_sealer), &expected_tail); + + let mut batched_sealer = IntervalSealer::new(origin); + let mut batched_snapshots = Vec::new(); + let mut offset = 0; + let mut batch_index = 0; + while offset < entries.len() { + let batch_size = batch_sizes[batch_index % batch_sizes.len()]; + let batch_end = (offset + batch_size).min(entries.len()); + batched_snapshots.extend( + batched_sealer + .push_entries(entries[offset..batch_end].iter().copied()) + .iter() + .map(|snapshot| normalize_snapshot(origin, snapshot)), + ); + offset = batch_end; + batch_index += 1; + } + prop_assert_eq!(batched_snapshots, expected_snapshots); + prop_assert_eq!(normalize_tail(origin, &batched_sealer), expected_tail); + } + + #[test] + fn occupancy_matches_interval_union_and_fold_apportionment( + event_spans in prop::collection::vec((0u8..=128, 0u8..=128), 0..=16), + small_poll_spans in prop::collection::vec( + (0u8..=128, 0u8..=128, 0u8..=4), + 0..=12, + ), + window in (0u8..=128, 0u8..=128), + ) { + let origin = Instant::now(); + let window_start_micros = window.0.min(window.1); + let window_end_micros = window.0.max(window.1); + let events = event_spans + .iter() + .map(|&(first, second)| { + let start = origin + Duration::from_micros(u64::from(first.min(second))); + let end = origin + Duration::from_micros(u64::from(first.max(second))); + ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end, + caused_invalidation: false, + }) + }) + .collect(); + let small_polls = small_poll_spans + .iter() + .map(|&(first, second, factor)| { + let since_micros = first.min(second); + let until_micros = first.max(second); + let span_micros = u64::from(until_micros - since_micros); + let total_micros = if span_micros == 0 { + u64::from(factor) + } else { + span_micros * u64::from(factor) + }; + SmallPollFlush { + summary: PollSummary { + count: 1, + total: Duration::from_micros(total_micros), + }, + since: origin + Duration::from_micros(u64::from(since_micros)), + until: origin + Duration::from_micros(u64::from(until_micros)), + } + }) + .collect(); + let snapshot = FrameSnapshot { + interval_start: origin, + boundary: IntervalBoundary::Idle { + ended_at: origin + Duration::from_micros(128), + }, + events, + small_polls, + dropped_events: 0, + journal_discontinuous: false, + }; + let expected_micros = reference_occupancy_micros( + &event_spans, + &small_poll_spans, + window_start_micros, + window_end_micros, + ); + let observed = snapshot.occupancy_within( + origin + Duration::from_micros(u64::from(window_start_micros)), + origin + Duration::from_micros(u64::from(window_end_micros)), + ); + + prop_assert_eq!(observed, Duration::from_micros(expected_micros)); + + let full_expected = + reference_occupancy_micros(&event_spans, &small_poll_spans, 0, 128); + prop_assert_eq!(snapshot.occupancy(), Duration::from_micros(full_expected)); + prop_assert!((0.0..=1.0).contains(&snapshot.busy_fraction())); + prop_assert_eq!( + snapshot.busy_fraction(), + (full_expected as f64 / 128.0).min(1.0) + ); + } + + #[test] + fn writer_matches_state_model_across_turn_frame_and_runnable_transitions( + operations in prop::collection::vec( + (any::(), any::(), any::()).prop_map( + |(kind, advance, argument)| WriterOperation { + kind, + advance, + argument, + }, + ), + 1..=128, + ), + ) { + let origin = Instant::now(); + let counter = ForegroundRunnableCounter::new(); + let (mut writer, mut collector) = test_journal(counter.clone()); + let mut model = ModelWriter::default(); + let mut at_micros = 100u64; + + for operation in operations { + if operation.advance % 16 == 0 { + at_micros += FRAME_DEADLINE.as_micros() as u64; + } else { + at_micros += u64::from(operation.advance) + 1; + } + let at = origin + Duration::from_micros(at_micros); + let window_id = WindowId::from(u64::from(operation.argument % 4)); + let window = window_id.as_u64(); + + match operation.kind % 10 { + 0 => { + writer.begin_turn(); + model.turn_depth += 1; + } + 1 if model.turn_depth > 0 => { + writer.end_turn(at); + model.end_turn(at_micros); + } + 1 => {} + 2 => { + writer.record_event(ForegroundEvent::Input(InputTiming { + kind: "test", + start: at, + end: at, + caused_invalidation: false, + })); + model.record_input(at_micros); + } + 3 => { + let duration_micros = u64::from(operation.argument % 100); + writer.fold_small_poll(task_timing( + at - Duration::from_micros(duration_micros), + at, + )); + model.fold_small_poll( + at_micros - duration_micros, + at_micros, + ); + } + 4 => { + counter.queued(); + model.runnables += 1; + } + 5 if model.runnables > 0 => { + counter.finished(); + model.runnables -= 1; + } + 5 => {} + 6 => { + writer.record_frame_pending(window_id, at); + model.record_pending(window, at_micros); + } + 7 => { + writer.record_window_closed(window_id, at); + model.record_closed(window, at_micros); + } + 8 => { + writer.record_present( + presentation_timing(window_id, at), + Some(frame_timing(window_id, at, at)), + ); + model.record_present(window, at_micros, true); + } + _ => { + writer.record_present(presentation_timing(window_id, at), None); + model.record_present(window, at_micros, false); + } + } + + let drained = collector.collect_unseen(); + prop_assert_eq!(drained.lost, 0); + prop_assert_eq!( + normalize_writer_entries(origin, drained.entries), + std::mem::take(&mut model.entries) + ); + prop_assert_eq!(writer.turn_depth, model.turn_depth); + prop_assert_eq!(writer.retained_since_boundary, model.retained_since_boundary); + prop_assert_eq!( + writer + .windows + .values() + .filter(|window| window.dirty_at.is_some()) + .count(), + model.pending_frames.len() + ); + } + } + } + + fn test_journal( + foreground_runnables: ForegroundRunnableCounter, + ) -> (ForegroundJournalWriter, ForegroundJournalCollector) { + let (journal, publisher) = ForegroundJournal::new(256, 8); + let collector = journal.collector(); + ( + ForegroundJournalWriter::new(foreground_runnables, publisher).with_test_suspend_clock(), + collector, + ) + } + + fn has_boundary_at(entries: &[ForegroundJournalEntry], at: Instant) -> bool { + entries.iter().any(|entry| { + matches!(entry, ForegroundJournalEntry::Boundary(boundary) if boundary.end_time() == at) + }) + } + + fn input_entry(start: Instant, end: Instant) -> ForegroundJournalEntry { + ForegroundJournalEntry::Event(ForegroundEvent::Input(InputTiming { + kind: "test", + start, + end, + caused_invalidation: false, + })) + } + + fn pending_frame(window_id: WindowId, dirty_at: Instant) -> ForegroundJournalEntry { + ForegroundJournalEntry::FrameState(FrameStateChange::Pending { + window_id, + dirty_at, + }) + } + + fn frame_timing(window_id: WindowId, dirty_at: Instant, draw_end: Instant) -> FrameTiming { + FrameTiming { + window_id, + dirty_at: Some(dirty_at), + invalidations: 1, + draw_start: draw_end, + draw_end, + } + } + + fn presentation_timing(window_id: WindowId, present_end: Instant) -> PresentTiming { + PresentTiming { + window_id, + present_start: present_end, + present_end, + animation_interval: None, + } + } + + fn presented_boundary( + window_id: WindowId, + dirty_at: Instant, + present_end: Instant, + ) -> IntervalBoundary { + IntervalBoundary::Presented(PresentedFrame { + frame: frame_timing(window_id, dirty_at, present_end), + presentation: presentation_timing(window_id, present_end), + }) + } + + fn task_timing(start: Instant, end: Instant) -> TaskTiming { + TaskTiming { + location: std::panic::Location::caller(), + spawned: SpawnTime(start), + start, + end: YieldTime(end), + } + } +} diff --git a/vendor/gpui-pre/src/queue.rs b/vendor/gpui-pre/src/queue.rs new file mode 100644 index 0000000..6d92130 --- /dev/null +++ b/vendor/gpui-pre/src/queue.rs @@ -0,0 +1,499 @@ +use std::{ + collections::VecDeque, + fmt, + iter::FusedIterator, + sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError, atomic::AtomicUsize}, +}; + +use rand::{Rng, SeedableRng, rngs::SmallRng}; + +use crate::Priority; + +struct PriorityQueues { + high_priority: VecDeque, + medium_priority: VecDeque, + low_priority: VecDeque, +} + +impl PriorityQueues { + fn is_empty(&self) -> bool { + self.high_priority.is_empty() + && self.medium_priority.is_empty() + && self.low_priority.is_empty() + } +} + +struct PriorityQueueState { + // std's Wasm atomic backend unlocks and notifies without acquiring another + // lock. parking_lot can park during those operations, even after try_lock. + queues: Mutex>, + condvar: Condvar, + receiver_count: AtomicUsize, + sender_count: AtomicUsize, +} + +impl PriorityQueueState { + fn send(&self, priority: Priority, item: T) -> Result<(), SendError> { + if self + .receiver_count + .load(std::sync::atomic::Ordering::Relaxed) + == 0 + { + return Err(SendError(item)); + } + + let mut queues = self.queues.lock().unwrap_or_else(PoisonError::into_inner); + Self::push(&mut queues, priority, item); + self.condvar.notify_one(); + Ok(()) + } + + fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError> { + if self + .receiver_count + .load(std::sync::atomic::Ordering::Relaxed) + == 0 + { + return Err(SendError(item)); + } + + let mut queues = loop { + match self.queues.try_lock() { + Ok(guard) => break guard, + Err(TryLockError::Poisoned(error)) => break error.into_inner(), + Err(TryLockError::WouldBlock) => std::hint::spin_loop(), + } + }; + Self::push(&mut queues, priority, item); + self.condvar.notify_one(); + Ok(()) + } + + fn push(queues: &mut PriorityQueues, priority: Priority, item: T) { + match priority { + Priority::RealtimeAudio => unreachable!( + "Realtime audio priority runs on a dedicated thread and is never queued" + ), + Priority::High => queues.high_priority.push_back(item), + Priority::Medium => queues.medium_priority.push_back(item), + Priority::Low => queues.low_priority.push_back(item), + }; + } + + fn recv<'a>(&'a self) -> Result>, RecvError> { + let mut queues = self.queues.lock().unwrap_or_else(PoisonError::into_inner); + + // Re-checked after every wake: dropping the last sender wakes waiting receivers + // so they can end instead of blocking forever on a queue nothing can fill. + while queues.is_empty() { + if self.sender_count.load(std::sync::atomic::Ordering::Acquire) == 0 { + return Err(crate::queue::RecvError); + } + queues = self + .condvar + .wait(queues) + .unwrap_or_else(PoisonError::into_inner); + } + + Ok(queues) + } + + fn try_recv<'a>(&'a self) -> Result>>, RecvError> { + let queues = self.queues.lock().unwrap_or_else(PoisonError::into_inner); + + let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed); + if queues.is_empty() && sender_count == 0 { + return Err(crate::queue::RecvError); + } + + if queues.is_empty() { + Ok(None) + } else { + Ok(Some(queues)) + } + } + + fn spin_try_recv<'a>(&'a self) -> Result>>, RecvError> { + let queues = loop { + match self.queues.try_lock() { + Ok(guard) => break guard, + Err(TryLockError::Poisoned(error)) => break error.into_inner(), + Err(TryLockError::WouldBlock) => std::hint::spin_loop(), + } + }; + + let sender_count = self.sender_count.load(std::sync::atomic::Ordering::Relaxed); + if queues.is_empty() && sender_count == 0 { + return Err(crate::queue::RecvError); + } + + if queues.is_empty() { + Ok(None) + } else { + Ok(Some(queues)) + } + } +} + +#[doc(hidden)] +pub struct PriorityQueueSender { + state: Arc>, +} + +impl PriorityQueueSender { + fn new(state: Arc>) -> Self { + Self { state } + } + + pub fn send(&self, priority: Priority, item: T) -> Result<(), SendError> { + self.state.send(priority, item)?; + Ok(()) + } + + pub fn spin_send(&self, priority: Priority, item: T) -> Result<(), SendError> { + self.state.spin_send(priority, item)?; + Ok(()) + } +} + +impl Drop for PriorityQueueSender { + fn drop(&mut self) { + let previous = self + .state + .sender_count + .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); + if previous == 1 { + // A receiver checks the sender count and starts waiting under the queue lock, + // atomically. Acquiring that lock once, even with nothing to do under it, + // means any receiver that saw a live sender is already waiting, so the + // notification below reaches it; without this, a receiver between its check + // and its wait would miss the wake-up and block forever. + drop( + self.state + .queues + .lock() + .unwrap_or_else(PoisonError::into_inner), + ); + self.state.condvar.notify_all(); + } + } +} + +#[doc(hidden)] +pub struct PriorityQueueReceiver { + state: Arc>, + rand: SmallRng, + disconnected: bool, +} + +impl Clone for PriorityQueueReceiver { + fn clone(&self) -> Self { + self.state + .receiver_count + .fetch_add(1, std::sync::atomic::Ordering::AcqRel); + Self { + state: Arc::clone(&self.state), + rand: SmallRng::seed_from_u64(0), + disconnected: self.disconnected, + } + } +} + +#[doc(hidden)] +pub struct SendError(pub T); + +impl fmt::Debug for SendError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("SendError").field(&self.0).finish() + } +} + +#[derive(Debug)] +#[doc(hidden)] +pub struct RecvError; + +#[allow(dead_code)] +impl PriorityQueueReceiver { + pub fn new() -> (PriorityQueueSender, Self) { + let state = PriorityQueueState { + queues: Mutex::new(PriorityQueues { + high_priority: VecDeque::new(), + medium_priority: VecDeque::new(), + low_priority: VecDeque::new(), + }), + condvar: Condvar::new(), + receiver_count: AtomicUsize::new(1), + sender_count: AtomicUsize::new(1), + }; + let state = Arc::new(state); + + let sender = PriorityQueueSender::new(Arc::clone(&state)); + + let receiver = PriorityQueueReceiver { + state, + rand: SmallRng::seed_from_u64(0), + disconnected: false, + }; + + (sender, receiver) + } + + /// Returns whether the queue currently contains no elements. + pub fn is_empty(&self) -> bool { + self.state + .queues + .lock() + .unwrap_or_else(PoisonError::into_inner) + .is_empty() + } + + /// Returns the number of queued elements across all priorities. + pub(crate) fn len(&self) -> usize { + let queues = self + .state + .queues + .lock() + .unwrap_or_else(PoisonError::into_inner); + queues.high_priority.len() + queues.medium_priority.len() + queues.low_priority.len() + } + + /// Tries to pop one element from the priority queue without blocking. + /// + /// This will early return if there are no elements in the queue. + /// + /// This method is best suited if you only intend to pop one element, for better performance + /// on large queues see [`Self::try_iter`] + /// + /// # Errors + /// + /// If the sender was dropped + pub fn try_pop(&mut self) -> Result, RecvError> { + self.pop_inner(false) + } + + pub fn spin_try_pop(&mut self) -> Result, RecvError> { + use Priority as P; + + let Some(mut queues) = self.state.spin_try_recv()? else { + return Ok(None); + }; + + let high = P::High.weight() * !queues.high_priority.is_empty() as u32; + let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32; + let low = P::Low.weight() * !queues.low_priority.is_empty() as u32; + let mut mass = high + medium + low; + + if !queues.high_priority.is_empty() { + let flip = self.rand.random_ratio(P::High.weight(), mass); + if flip { + return Ok(queues.high_priority.pop_front()); + } + mass -= P::High.weight(); + } + + if !queues.medium_priority.is_empty() { + let flip = self.rand.random_ratio(P::Medium.weight(), mass); + if flip { + return Ok(queues.medium_priority.pop_front()); + } + mass -= P::Medium.weight(); + } + + if !queues.low_priority.is_empty() { + let flip = self.rand.random_ratio(P::Low.weight(), mass); + if flip { + return Ok(queues.low_priority.pop_front()); + } + } + + Ok(None) + } + + /// Pops an element from the priority queue blocking if necessary. + /// + /// This method is best suited if you only intend to pop one element, for better performance + /// on large queues see [`Self::iter``] + /// + /// # Errors + /// + /// If the sender was dropped + pub fn pop(&mut self) -> Result { + self.pop_inner(true).map(|e| e.unwrap()) + } + + /// Returns an iterator over the elements of the queue + /// this iterator will end when all elements have been consumed and will not wait for new ones. + pub fn try_iter(self) -> TryIter { + TryIter { + receiver: self, + ended: false, + } + } + + /// Returns an iterator over the elements of the queue + /// this iterator will wait for new elements if the queue is empty. + pub fn iter(self) -> Iter { + Iter(self) + } + + #[inline(always)] + // algorithm is the loaded die from biased coin from + // https://www.keithschwarz.com/darts-dice-coins/ + fn pop_inner(&mut self, block: bool) -> Result, RecvError> { + use Priority as P; + + let mut queues = if !block { + let Some(queues) = self.state.try_recv()? else { + return Ok(None); + }; + queues + } else { + self.state.recv()? + }; + + let high = P::High.weight() * !queues.high_priority.is_empty() as u32; + let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32; + let low = P::Low.weight() * !queues.low_priority.is_empty() as u32; + let mut mass = high + medium + low; //% + + if !queues.high_priority.is_empty() { + let flip = self.rand.random_ratio(P::High.weight(), mass); + if flip { + return Ok(queues.high_priority.pop_front()); + } + mass -= P::High.weight(); + } + + if !queues.medium_priority.is_empty() { + let flip = self.rand.random_ratio(P::Medium.weight(), mass); + if flip { + return Ok(queues.medium_priority.pop_front()); + } + mass -= P::Medium.weight(); + } + + if !queues.low_priority.is_empty() { + let flip = self.rand.random_ratio(P::Low.weight(), mass); + if flip { + return Ok(queues.low_priority.pop_front()); + } + } + + Ok(None) + } +} + +impl Drop for PriorityQueueReceiver { + fn drop(&mut self) { + self.state + .receiver_count + .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); + } +} + +#[doc(hidden)] +pub struct Iter(PriorityQueueReceiver); +impl Iterator for Iter { + type Item = T; + + fn next(&mut self) -> Option { + self.0.pop().ok() + } +} +impl FusedIterator for Iter {} + +#[doc(hidden)] +pub struct TryIter { + receiver: PriorityQueueReceiver, + ended: bool, +} +impl Iterator for TryIter { + type Item = Result; + + fn next(&mut self) -> Option { + if self.ended { + return None; + } + + let res = self.receiver.try_pop(); + self.ended = res.is_err(); + + res.transpose() + } +} +impl FusedIterator for TryIter {} + +#[cfg(test)] +mod tests { + use collections::HashSet; + + use super::*; + + #[test] + fn blocked_receivers_end_when_the_sender_drops() { + let (sender, receiver) = PriorityQueueReceiver::::new(); + let (done_sender, done_receiver) = std::sync::mpsc::channel(); + let workers = (0..2) + .map(|_| { + let receiver = receiver.clone(); + let done_sender = done_sender.clone(); + std::thread::spawn(move || { + let received: Vec = receiver.iter().collect(); + done_sender.send(received).ok(); + }) + }) + .collect::>(); + drop(receiver); + + sender.send(Priority::Medium, 7).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!( + done_receiver.try_recv().is_err(), + "receivers keep waiting while the sender is alive" + ); + + drop(sender); + let mut received = Vec::new(); + for _ in 0..2 { + received.extend( + done_receiver + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("a waiting receiver should end once the sender is dropped"), + ); + } + assert_eq!(received, [7]); + for worker in workers { + worker.join().expect("worker should exit cleanly"); + } + } + + #[test] + fn all_tasks_get_yielded() { + let (tx, mut rx) = PriorityQueueReceiver::new(); + tx.send(Priority::Medium, 20).unwrap(); + tx.send(Priority::High, 30).unwrap(); + tx.send(Priority::Low, 10).unwrap(); + tx.send(Priority::Medium, 21).unwrap(); + tx.send(Priority::High, 31).unwrap(); + + drop(tx); + + assert_eq!( + rx.iter().collect::>(), + [30, 31, 20, 21, 10].into_iter().collect::>() + ) + } + + #[test] + fn new_high_prio_task_get_scheduled_quickly() { + let (tx, mut rx) = PriorityQueueReceiver::new(); + for _ in 0..100 { + tx.send(Priority::Low, 1).unwrap(); + } + + assert_eq!(rx.pop().unwrap(), 1); + tx.send(Priority::High, 3).unwrap(); + assert_eq!(rx.pop().unwrap(), 3); + assert_eq!(rx.pop().unwrap(), 1); + } +} diff --git a/vendor/gpui-pre/src/randomized_element_tree.rs b/vendor/gpui-pre/src/randomized_element_tree.rs new file mode 100644 index 0000000..1366317 --- /dev/null +++ b/vendor/gpui-pre/src/randomized_element_tree.rs @@ -0,0 +1,1745 @@ +use crate::{ + AnyElement, AppContext as _, Context, ElementId, Entity, Hsla, InteractiveElement, IntoElement, + ParentElement, Render, StatefulInteractiveElement, Styled, Window, div, px, + util::FluentBuilder, +}; +use collections::{HashMap, HashSet}; +use rand::{Rng as _, SeedableRng as _, rngs::StdRng}; +use std::{ + cell::RefCell, + ops::RangeInclusive, + rc::{Rc, Weak}, +}; + +/// Dispatched to no one: the action generated elements register handlers for. Not +/// registered, so builds that enable `bench-support` through feature unification (such as +/// the workspace's tests) don't gain an action namespace. +#[derive(Clone, Default, PartialEq, crate::Action)] +#[action(namespace = randomized_element_tree, no_json, no_register)] +pub struct RandomizedElementTreeAction; + +/// The initial shape generated for a randomized element tree. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RandomizedElementTreeTopology { + /// Every generated element is a direct child of the root. + Wide, + /// Every generated element is the child of the previous element. + Deep, + /// Generated elements choose parents throughout the existing tree. + Mixed, + /// Generated elements usually extend one deep branch, with occasional root children. + Narrow, +} + +impl RandomizedElementTreeTopology { + /// Every topology. + pub const ALL: [Self; 4] = [Self::Wide, Self::Deep, Self::Mixed, Self::Narrow]; + + /// Whether the tree's depth grows with its element count. `Deep` nests every element + /// under the previous one and `Narrow` nests four in five, so both recurse once per + /// element through layout and paint; without gpui's `stacker` feature that overflows + /// a 2 MB thread stack somewhere between 384 and 512 elements in a release build. + pub fn is_tall(self) -> bool { + matches!(self, Self::Deep | Self::Narrow) + } + + fn from_seed(seed: u64) -> Self { + match seed % 4 { + 0 => Self::Wide, + 1 => Self::Deep, + 2 => Self::Mixed, + _ => Self::Narrow, + } + } +} + +/// Configuration for a reproducible randomized element-tree workload. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RandomizedElementTreeConfig { + seed: u64, + element_count: usize, + topology: RandomizedElementTreeTopology, + entity_density: f64, + handler_density: f64, +} + +impl RandomizedElementTreeConfig { + /// Creates an element-only workload whose topology is selected by `seed`. + pub fn new(seed: u64, element_count: usize) -> Self { + Self { + seed, + element_count, + topology: RandomizedElementTreeTopology::from_seed(seed), + entity_density: 0.0, + handler_density: 0.0, + } + } + + /// Selects the initial tree topology independently of the RNG seed. + pub fn with_topology(mut self, topology: RandomizedElementTreeTopology) -> Self { + self.topology = topology; + self + } + + /// Sets the probability that each generated element is backed by a persistent entity. + pub fn with_entity_density(mut self, density: f64) -> Self { + self.entity_density = normalized_density(density); + self + } + + /// Sets the probability of each generated interaction feature on an element. + pub fn with_handler_density(mut self, density: f64) -> Self { + self.handler_density = normalized_density(density); + self + } + + /// Returns the seed used for generation and subsequent random mutations. + pub fn seed(&self) -> u64 { + self.seed + } + + /// Returns the requested number of initial descendants. + pub fn element_count(&self) -> usize { + self.element_count + } + + /// Returns the selected initial topology. + pub fn topology(&self) -> RandomizedElementTreeTopology { + self.topology + } + + /// Returns the probability that a generated element is backed by an entity. + pub fn entity_density(&self) -> f64 { + self.entity_density + } + + /// Returns the probability of each generated interaction feature. + pub fn handler_density(&self) -> f64 { + self.handler_density + } +} + +/// Bounds an RNG is drawn against to produce a [`RandomizedElementTreeConfig`]: a family +/// of trees, such as "tall trees of 32 to 256 elements with up to half of them entities", +/// from which one draw picks one member. Every field of the resulting config, and the +/// tree built from it, follows from the RNG's state, so an RNG seeded by `#[gpui::bench]` +/// or `#[gpui::test]` samples the family across seeds and any one seed reproduces exactly. +#[derive(Clone, Debug, PartialEq)] +pub struct RandomizedElementTreeBounds { + topologies: Vec, + element_count: RangeInclusive, + entity_density: RangeInclusive, + handler_density: RangeInclusive, +} + +impl RandomizedElementTreeBounds { + /// Trees of any topology with `element_count` elements, no entities and no handlers. + pub fn new(element_count: RangeInclusive) -> Self { + Self { + topologies: RandomizedElementTreeTopology::ALL.to_vec(), + element_count, + entity_density: 0.0..=0.0, + handler_density: 0.0..=0.0, + } + } + + /// Restricts the topologies a seed may pick. An empty list keeps them all. + pub fn with_topologies( + mut self, + topologies: impl IntoIterator, + ) -> Self { + let topologies: Vec<_> = topologies.into_iter().collect(); + if !topologies.is_empty() { + self.topologies = topologies; + } + self + } + + /// The range a seed draws the share of entity-backed elements from. + pub fn with_entity_density(mut self, density: RangeInclusive) -> Self { + self.entity_density = + normalized_density(*density.start())..=normalized_density(*density.end()); + self + } + + /// The range a seed draws the per-feature handler probability from. + pub fn with_handler_density(mut self, density: RangeInclusive) -> Self { + self.handler_density = + normalized_density(*density.start())..=normalized_density(*density.end()); + self + } + + /// Draws one config from these bounds. The config's own seed, which drives the tree's + /// contents and mutation stream, is drawn from `rng` too, so all of it reproduces from + /// the seed `rng` started from. + pub fn sample(&self, rng: &mut StdRng) -> RandomizedElementTreeConfig { + let topology = self.topologies[rng.random_range(0..self.topologies.len())]; + let element_count = rng.random_range(self.element_count.clone()); + RandomizedElementTreeConfig::new(rng.random(), element_count) + .with_topology(topology) + .with_entity_density(sample_range(&self.entity_density, rng)) + .with_handler_density(sample_range(&self.handler_density, rng)) + } +} + +fn sample_range(range: &RangeInclusive, rng: &mut StdRng) -> f64 { + if range.start() >= range.end() { + *range.start() + } else { + rng.random_range(range.clone()) + } +} + +/// Work performed while converting randomized entities and elements into an element tree. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct RandomizedElementTreeWorkCounters { + root_render_count: usize, + entity_render_count: usize, + element_render_count: usize, + handler_registration_count: usize, + rendered_entity_ids: Vec, +} + +impl RandomizedElementTreeWorkCounters { + /// Returns how many times the persistent root's `Render` implementation ran. + pub fn root_render_count(&self) -> usize { + self.root_render_count + } + + /// Returns how many randomized child entities rendered. + pub fn entity_render_count(&self) -> usize { + self.entity_render_count + } + + /// Returns how many randomized descendant elements were reconstructed. + pub fn element_render_count(&self) -> usize { + self.element_render_count + } + + /// Returns how many interaction handlers were registered while rendering. + pub fn handler_registration_count(&self) -> usize { + self.handler_registration_count + } + + /// Returns stable IDs for the randomized child entities that rendered, in render order. + pub fn rendered_entity_ids(&self) -> &[u64] { + &self.rendered_entity_ids + } + + fn reset(&mut self) { + self.root_render_count = 0; + self.entity_render_count = 0; + self.element_render_count = 0; + self.handler_registration_count = 0; + self.rendered_entity_ids.clear(); + } +} + +/// A cloneable description of a randomized element tree's renderable state. +#[derive(Clone, Debug, PartialEq)] +pub struct RandomizedElementTreeSnapshot { + root: RandomizedElementStyle, + children: Vec, + next_element_id: u64, + paths_by_element_id: HashMap>, +} + +impl RandomizedElementTreeSnapshot { + /// Returns the number of randomized elements below the persistent root. + pub fn descendant_count(&self) -> usize { + let mut count = 0; + let mut stack = self.children.iter().collect::>(); + while let Some(node) = stack.pop() { + count += 1; + stack.extend(node.children.iter()); + } + count + } + + /// Returns the deepest randomized element below the persistent root. + /// + /// A snapshot with no descendants has depth zero. + pub fn max_depth(&self) -> usize { + let mut max_depth = 0; + let mut stack = self + .children + .iter() + .map(|node| (node, 1)) + .collect::>(); + while let Some((node, depth)) = stack.pop() { + max_depth = max_depth.max(depth); + stack.extend(node.children.iter().map(|child| (child, depth + 1))); + } + max_depth + } + + /// Returns the stable element IDs in depth-first order. + pub fn element_ids(&self) -> Vec { + node_paths(&self.children) + .iter() + .filter_map(|path| node_at_path(&self.children, path).map(|node| node.id)) + .collect() + } + + /// Returns the number of descendants backed by persistent child entities. + pub fn entity_count(&self) -> usize { + let mut count = 0; + let mut stack = self.children.iter().collect::>(); + while let Some(node) = stack.pop() { + count += usize::from(node.is_entity); + stack.extend(node.children.iter()); + } + count + } + + /// Returns the number of interaction handlers in the generated tree. + pub fn handler_count(&self) -> usize { + let mut count = 0; + let mut stack = self.children.iter().collect::>(); + while let Some(node) = stack.pop() { + count += node.features.handler_count(); + stack.extend(node.children.iter()); + } + count + } + + fn rebuild_element_paths(&mut self) { + self.paths_by_element_id = node_paths(&self.children) + .into_iter() + .filter_map(|path| node_at_path(&self.children, &path).map(|node| (node.id, path))) + .collect(); + } + + fn node(&self, element_id: u64) -> Option<&RandomizedElementNode> { + node_at_path(&self.children, self.paths_by_element_id.get(&element_id)?) + } +} + +/// A requested class of deterministic randomized-tree mutation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RandomizedElementTreeMutationKind { + /// Change the root background color. + RootColor, + /// Change the root opacity. + RootOpacity, + /// Change the root border. + RootBorder, + /// Change the root width and height. + RootBounds, + /// Toggle clipping on the root. + RootClipping, + /// Toggle root visibility. + RootVisibility, + /// Change a seeded random descendant's colors. + ChildColor, + /// Change a seeded random descendant's opacity. + ChildOpacity, + /// Change a seeded random descendant's border. + ChildBorder, + /// Change a seeded random descendant's width and height. + ChildBounds, + /// Toggle clipping on a seeded random descendant. + ChildClipping, + /// Toggle visibility on a seeded random descendant. + ChildVisibility, + /// Reorder a seeded random descendant among its siblings. + Reorder, + /// Insert a generated descendant below a seeded random parent. + Insert, + /// Remove a seeded random descendant and its subtree. + Remove, + /// Remove a seeded random childless descendant. Unlike [`Self::Remove`] this changes + /// the element count by exactly one, so it can alternate with [`Self::Insert`] in a + /// long benchmark loop without the tree collapsing or growing. + RemoveLeaf, +} + +impl RandomizedElementTreeMutationKind { + /// Every supported mutation kind, suitable for parameterized tests and benchmarks. + pub const ALL: [Self; 16] = [ + Self::RootColor, + Self::RootOpacity, + Self::RootBorder, + Self::RootBounds, + Self::RootClipping, + Self::RootVisibility, + Self::ChildColor, + Self::ChildOpacity, + Self::ChildBorder, + Self::ChildBounds, + Self::ChildClipping, + Self::ChildVisibility, + Self::Reorder, + Self::Insert, + Self::Remove, + Self::RemoveLeaf, + ]; +} + +/// A deterministic mutation applied to a [`RandomizedElementTree`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RandomizedElementTreeMutation { + /// Changed the root background color. + RootColor, + /// Changed the root opacity. + RootOpacity, + /// Changed the root border. + RootBorder, + /// Changed the root width and height. + RootBounds, + /// Toggled clipping on the root. + RootClipping, + /// Toggled root visibility. + RootVisibility, + /// Changed a descendant's colors. + ChildColor { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Changed a descendant's opacity. + ChildOpacity { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Changed a descendant's border. + ChildBorder { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Changed a descendant's width and height. + ChildBounds { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Toggled clipping on a descendant. + ChildClipping { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Toggled a descendant's visibility. + ChildVisibility { + /// The stable ID of the mutated element. + element_id: u64, + }, + /// Moved an element to a different position among its siblings. + Reordered { + /// The stable ID of the moved element. + element_id: u64, + /// The stable ID of its parent, or `None` for the persistent root. + parent_id: Option, + /// The element's previous position. + old_index: usize, + /// The element's new position. + new_index: usize, + }, + /// Inserted a newly generated element. + Inserted { + /// The stable ID of the inserted element. + element_id: u64, + /// The stable ID of its parent, or `None` for the persistent root. + parent_id: Option, + /// The position of the inserted element. + index: usize, + }, + /// Removed an element and all of its descendants. + Removed { + /// The stable ID of the removed element. + element_id: u64, + /// The stable ID of its parent, or `None` for the persistent root. + parent_id: Option, + /// The former position of the removed element. + index: usize, + /// The total number of elements removed with the subtree. + removed_element_count: usize, + }, +} + +/// Where the elements changed by one frame sit relative to each other. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ChangeLocality { + /// Inside one random entity's subtree, as a user action or one component's state + /// change would be; the change reaches the rest of the tree only if the subtree has + /// fewer elements than are changing (a common ancestor changing). + Localized, + /// Anywhere in the tree, uniformly: the worst case for a renderer that reuses clean + /// subtrees, since every changed element may sit in a different one. + Spread, +} + +/// What [`RandomizedElementTree::recolor_children`] changed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RecoloredChildren { + /// Descendants whose background changed. + pub recolored: usize, + /// Entity boundaries (or the root) notified, each once. + pub notified: usize, +} + +/// A persistent root view for randomized GPUI element-tree tests and benchmarks. +pub struct RandomizedElementTree { + snapshot: Rc>, + entities: Rc>, + work_counters: Rc>, + config: RandomizedElementTreeConfig, + rng: StdRng, +} + +impl RandomizedElementTree { + /// Generates a tree with `element_count` descendants and a mutation stream from `seed`. + /// + /// Different seeds deliberately produce wide, deep, mixed, and narrow initial topologies. + pub fn new(seed: u64, element_count: usize) -> Self { + Self::from_config(RandomizedElementTreeConfig::new(seed, element_count)) + } + + /// Generates a configured tree and creates its persistent child entities. + pub fn new_with_config(config: RandomizedElementTreeConfig, cx: &mut Context) -> Self { + let mut tree = Self::from_config(config); + tree.synchronize_entities(cx); + tree + } + + fn from_config(config: RandomizedElementTreeConfig) -> Self { + let mut rng = StdRng::seed_from_u64(config.seed); + let mut children = Vec::new(); + let mut deepest_path = Vec::new(); + + for element_index in 0..config.element_count { + let mut parent_path = match config.topology { + RandomizedElementTreeTopology::Wide => Vec::new(), + RandomizedElementTreeTopology::Deep => deepest_path.clone(), + RandomizedElementTreeTopology::Mixed => random_parent_path(&children, &mut rng), + RandomizedElementTreeTopology::Narrow => { + if rng.random_bool(0.8) { + deepest_path.clone() + } else { + Vec::new() + } + } + }; + let node = RandomizedElementNode::random( + element_index as u64, + config.entity_density, + config.handler_density, + &mut rng, + ); + let (_, inserted_path) = insert_node(&mut children, &mut parent_path, node); + deepest_path = inserted_path; + } + + let mut snapshot = RandomizedElementTreeSnapshot { + root: RandomizedElementStyle::random_root(&mut rng), + children, + next_element_id: config.element_count as u64, + paths_by_element_id: HashMap::default(), + }; + snapshot.rebuild_element_paths(); + + Self { + snapshot: Rc::new(RefCell::new(snapshot)), + entities: Rc::new(RefCell::new(HashMap::default())), + work_counters: Rc::new(RefCell::new(RandomizedElementTreeWorkCounters::default())), + config, + rng, + } + } + + /// Reconstructs a tree from renderable state captured by [`Self::snapshot`]. + pub fn from_snapshot(snapshot: RandomizedElementTreeSnapshot) -> Self { + Self { + config: RandomizedElementTreeConfig::new(0, snapshot.descendant_count()), + snapshot: Rc::new(RefCell::new(snapshot)), + entities: Rc::new(RefCell::new(HashMap::default())), + work_counters: Rc::new(RefCell::new(RandomizedElementTreeWorkCounters::default())), + rng: StdRng::seed_from_u64(0), + } + } + + /// Reconstructs a snapshot while preserving its randomized entity boundaries. + pub fn from_snapshot_with_entities( + snapshot: RandomizedElementTreeSnapshot, + cx: &mut Context, + ) -> Self { + let mut tree = Self::from_snapshot(snapshot); + tree.synchronize_entities(cx); + tree + } + + /// Captures all state that affects rendering and future element IDs. + pub fn snapshot(&self) -> RandomizedElementTreeSnapshot { + self.snapshot.borrow().clone() + } + + /// Clears render work accumulated since construction or the previous reset. + pub fn reset_work_counters(&mut self) { + self.work_counters.borrow_mut().reset(); + } + + /// Returns work accumulated since construction or the previous reset. + pub fn work_counters(&self) -> RandomizedElementTreeWorkCounters { + self.work_counters.borrow().clone() + } + + /// Applies one seeded random style or structural mutation. + /// + /// This does not notify persistent child entities. Entity-aware callers should use + /// [`Self::apply_random_mutation`] from an entity update instead. + pub fn mutate(&mut self) -> RandomizedElementTreeMutation { + let kind = RandomizedElementTreeMutationKind::ALL[self + .rng + .random_range(0..RandomizedElementTreeMutationKind::ALL.len())]; + self.apply_mutation_without_notification(kind).mutation + } + + /// Applies one seeded random mutation and notifies its nearest persistent entity boundary. + pub fn apply_random_mutation( + &mut self, + cx: &mut Context, + ) -> RandomizedElementTreeMutation { + let kind = RandomizedElementTreeMutationKind::ALL[self + .rng + .random_range(0..RandomizedElementTreeMutationKind::ALL.len())]; + self.apply_mutation(kind, cx) + } + + /// Applies a selected mutation kind and notifies its nearest persistent entity boundary. + pub fn apply_mutation( + &mut self, + kind: RandomizedElementTreeMutationKind, + cx: &mut Context, + ) -> RandomizedElementTreeMutation { + let applied = self.apply_mutation_without_notification(kind); + if applied.synchronize_entities { + self.synchronize_entities(cx); + } + self.notify_owner(applied.owner_entity_id, cx); + applied.mutation + } + + /// Generates and inserts an element below a seeded random parent. + /// + /// This does not synchronize or notify persistent child entities. Entity-aware callers should + /// use [`Self::apply_mutation`] with [`RandomizedElementTreeMutationKind::Insert`]. + pub fn insert_child(&mut self) -> RandomizedElementTreeMutation { + self.insert_child_internal().mutation + } + + /// Removes a seeded random element and its subtree. + /// + /// This does not synchronize or notify persistent child entities. Entity-aware callers should + /// use [`Self::apply_mutation`] with [`RandomizedElementTreeMutationKind::Remove`]. + pub fn remove_child(&mut self) -> Option { + self.remove_child_internal(false) + .map(|applied| applied.mutation) + } + + fn apply_mutation_without_notification( + &mut self, + kind: RandomizedElementTreeMutationKind, + ) -> AppliedRandomizedElementTreeMutation { + match kind { + RandomizedElementTreeMutationKind::RootColor => { + self.snapshot.borrow_mut().root.background = random_color(&mut self.rng); + AppliedRandomizedElementTreeMutation::root(RandomizedElementTreeMutation::RootColor) + } + RandomizedElementTreeMutationKind::RootOpacity => { + self.snapshot.borrow_mut().root.opacity = random_opacity(&mut self.rng); + AppliedRandomizedElementTreeMutation::root( + RandomizedElementTreeMutation::RootOpacity, + ) + } + RandomizedElementTreeMutationKind::RootBorder => { + self.snapshot + .borrow_mut() + .root + .randomize_border(&mut self.rng); + AppliedRandomizedElementTreeMutation::root( + RandomizedElementTreeMutation::RootBorder, + ) + } + RandomizedElementTreeMutationKind::RootBounds => { + let mut snapshot = self.snapshot.borrow_mut(); + snapshot.root.width = self.rng.random_range(520.0..980.0); + snapshot.root.height = self.rng.random_range(360.0..760.0); + AppliedRandomizedElementTreeMutation::root( + RandomizedElementTreeMutation::RootBounds, + ) + } + RandomizedElementTreeMutationKind::RootClipping => { + let mut snapshot = self.snapshot.borrow_mut(); + snapshot.root.clipped = !snapshot.root.clipped; + AppliedRandomizedElementTreeMutation::root( + RandomizedElementTreeMutation::RootClipping, + ) + } + RandomizedElementTreeMutationKind::RootVisibility => { + let mut snapshot = self.snapshot.borrow_mut(); + snapshot.root.visible = !snapshot.root.visible; + AppliedRandomizedElementTreeMutation::root( + RandomizedElementTreeMutation::RootVisibility, + ) + } + RandomizedElementTreeMutationKind::ChildColor => { + self.mutate_random_child(RandomizedChildMutation::Color) + } + RandomizedElementTreeMutationKind::ChildOpacity => { + self.mutate_random_child(RandomizedChildMutation::Opacity) + } + RandomizedElementTreeMutationKind::ChildBorder => { + self.mutate_random_child(RandomizedChildMutation::Border) + } + RandomizedElementTreeMutationKind::ChildBounds => { + self.mutate_random_child(RandomizedChildMutation::Bounds) + } + RandomizedElementTreeMutationKind::ChildClipping => { + self.mutate_random_child(RandomizedChildMutation::Clipping) + } + RandomizedElementTreeMutationKind::ChildVisibility => { + self.mutate_random_child(RandomizedChildMutation::Visibility) + } + RandomizedElementTreeMutationKind::Reorder => self.reorder_child(), + RandomizedElementTreeMutationKind::Insert => self.insert_child_internal(), + RandomizedElementTreeMutationKind::Remove => self + .remove_child_internal(false) + .unwrap_or_else(|| self.insert_child_internal()), + RandomizedElementTreeMutationKind::RemoveLeaf => self + .remove_child_internal(true) + .unwrap_or_else(|| self.insert_child_internal()), + } + } + + fn insert_child_internal(&mut self) -> AppliedRandomizedElementTreeMutation { + let mut snapshot = self.snapshot.borrow_mut(); + let element_id = snapshot.next_element_id; + snapshot.next_element_id += 1; + let mut parent_path = random_parent_path(&snapshot.children, &mut self.rng); + let parent_id = node_at_path(&snapshot.children, &parent_path).map(|node| node.id); + let owner_entity_id = nearest_entity_id(&snapshot.children, &parent_path); + let node = RandomizedElementNode::random( + element_id, + self.config.entity_density, + self.config.handler_density, + &mut self.rng, + ); + let (index, _) = insert_node(&mut snapshot.children, &mut parent_path, node); + snapshot.rebuild_element_paths(); + + AppliedRandomizedElementTreeMutation { + mutation: RandomizedElementTreeMutation::Inserted { + element_id, + parent_id, + index, + }, + owner_entity_id, + synchronize_entities: true, + } + } + + fn remove_child_internal( + &mut self, + leaves_only: bool, + ) -> Option { + let mut snapshot = self.snapshot.borrow_mut(); + let path = if leaves_only { + random_leaf_path(&snapshot.children, &mut self.rng)? + } else { + random_node_path(&snapshot.children, &mut self.rng)? + }; + let (index, parent_path) = path.split_last()?; + let parent_id = node_at_path(&snapshot.children, parent_path).map(|node| node.id); + let owner_entity_id = nearest_entity_id(&snapshot.children, parent_path); + let siblings = children_at_path_mut(&mut snapshot.children, parent_path)?; + if *index >= siblings.len() { + return None; + } + + let removed = siblings.remove(*index); + let removed_element_count = removed.element_count(); + let element_id = removed.id; + snapshot.rebuild_element_paths(); + Some(AppliedRandomizedElementTreeMutation { + mutation: RandomizedElementTreeMutation::Removed { + element_id, + parent_id, + index: *index, + removed_element_count, + }, + owner_entity_id, + synchronize_entities: true, + }) + } + + /// Recolors `count` distinct seeded random descendants in one pass and notifies each + /// affected entity boundary once (the root, for descendants owned by no entity). + /// Returns how many descendants changed and how many boundaries were notified. + /// + /// This is the "share of the scene changing per frame" knob: `count` calls to + /// [`Self::apply_mutation`] would each walk the tree and notify separately, costing + /// more than the frame being measured once `count` is a fair share of the tree. + pub fn recolor_children( + &mut self, + count: usize, + locality: ChangeLocality, + cx: &mut Context, + ) -> RecoloredChildren { + let owners = { + let mut snapshot = self.snapshot.borrow_mut(); + let mut paths = node_paths(&snapshot.children); + let count = count.min(paths.len()); + if locality == ChangeLocality::Localized { + // Order the paths so one random entity's subtree comes first: the change + // stays inside that entity unless there is more of it than the subtree. + let entity_paths: Vec<_> = paths + .iter() + .filter(|path| { + node_at_path(&snapshot.children, path).is_some_and(|node| node.is_entity) + }) + .cloned() + .collect(); + if let Some(entity_path) = entity_paths + .get(self.rng.random_range(0..entity_paths.len().max(1))) + .cloned() + { + let (inside, outside): (Vec<_>, Vec<_>) = paths + .into_iter() + .partition(|path| path.starts_with(&entity_path)); + paths = inside; + paths.extend(outside); + } + } + // A partial Fisher–Yates shuffle over the first `count` slots: for `Spread`, a + // uniform sample of the tree; for `Localized`, a uniform sample of the chosen + // subtree, spilling into the rest only once the subtree is exhausted. + let sample_from = match locality { + ChangeLocality::Spread => paths.len(), + ChangeLocality::Localized => count, + }; + for index in 0..count { + let other = self.rng.random_range(index..sample_from.max(index + 1)); + paths.swap(index, other); + } + let mut owners = HashSet::default(); + for path in &paths[..count] { + owners.insert(nearest_entity_id(&snapshot.children, path)); + if let Some(node) = node_at_path_mut(&mut snapshot.children, path) { + node.style.background = random_color(&mut self.rng); + } + } + owners + }; + let notified = owners.len(); + let recolored = count.min(self.snapshot.borrow().descendant_count()); + for owner in owners { + self.notify_owner(owner, cx); + } + RecoloredChildren { + recolored, + notified, + } + } + + fn mutate_random_child( + &mut self, + mutation: RandomizedChildMutation, + ) -> AppliedRandomizedElementTreeMutation { + let mut snapshot = self.snapshot.borrow_mut(); + let Some(path) = random_node_path(&snapshot.children, &mut self.rng) else { + drop(snapshot); + return self.insert_child_internal(); + }; + let owner_entity_id = nearest_entity_id(&snapshot.children, &path); + let Some(node) = node_at_path_mut(&mut snapshot.children, &path) else { + drop(snapshot); + return self.insert_child_internal(); + }; + let element_id = node.id; + + let mutation = match mutation { + RandomizedChildMutation::Color => { + node.style.background = random_color(&mut self.rng); + RandomizedElementTreeMutation::ChildColor { element_id } + } + RandomizedChildMutation::Opacity => { + node.style.opacity = random_opacity(&mut self.rng); + RandomizedElementTreeMutation::ChildOpacity { element_id } + } + RandomizedChildMutation::Border => { + node.style.randomize_border(&mut self.rng); + RandomizedElementTreeMutation::ChildBorder { element_id } + } + RandomizedChildMutation::Bounds => { + node.style.width = self.rng.random_range(36.0..220.0); + node.style.height = self.rng.random_range(28.0..180.0); + RandomizedElementTreeMutation::ChildBounds { element_id } + } + RandomizedChildMutation::Clipping => { + node.style.clipped = !node.style.clipped; + RandomizedElementTreeMutation::ChildClipping { element_id } + } + RandomizedChildMutation::Visibility => { + node.style.visible = !node.style.visible; + RandomizedElementTreeMutation::ChildVisibility { element_id } + } + }; + AppliedRandomizedElementTreeMutation { + mutation, + owner_entity_id, + synchronize_entities: false, + } + } + + fn reorder_child(&mut self) -> AppliedRandomizedElementTreeMutation { + let mut snapshot = self.snapshot.borrow_mut(); + let parent_paths = parent_paths_with_multiple_children(&snapshot.children); + if parent_paths.is_empty() { + drop(snapshot); + return self.insert_child_internal(); + } + + let parent_path_index = self.rng.random_range(0..parent_paths.len()); + let Some(parent_path) = parent_paths.get(parent_path_index) else { + drop(snapshot); + return self.insert_child_internal(); + }; + let parent_id = node_at_path(&snapshot.children, parent_path).map(|node| node.id); + let owner_entity_id = nearest_entity_id(&snapshot.children, parent_path); + let Some(child_count) = + children_at_path(&snapshot.children, parent_path).map(<[RandomizedElementNode]>::len) + else { + drop(snapshot); + return self.insert_child_internal(); + }; + if child_count < 2 { + drop(snapshot); + return self.insert_child_internal(); + } + + let old_index = self.rng.random_range(0..child_count); + let mut new_index = self.rng.random_range(0..child_count - 1); + if new_index >= old_index { + new_index += 1; + } + + let Some(siblings) = children_at_path_mut(&mut snapshot.children, parent_path) else { + drop(snapshot); + return self.insert_child_internal(); + }; + let child = siblings.remove(old_index); + let element_id = child.id; + siblings.insert(new_index, child); + snapshot.rebuild_element_paths(); + AppliedRandomizedElementTreeMutation { + mutation: RandomizedElementTreeMutation::Reordered { + element_id, + parent_id, + old_index, + new_index, + }, + owner_entity_id, + synchronize_entities: false, + } + } + + fn synchronize_entities(&mut self, cx: &mut Context) { + let entity_ids = { + let snapshot = self.snapshot.borrow(); + snapshot + .element_ids() + .into_iter() + .filter(|element_id| { + snapshot + .node(*element_id) + .is_some_and(|node| node.is_entity) + }) + .collect::>() + }; + let entity_id_set = entity_ids.iter().copied().collect::>(); + + self.entities + .borrow_mut() + .retain(|element_id, _| entity_id_set.contains(element_id)); + + let missing_entity_ids = { + let entities = self.entities.borrow(); + entity_ids + .into_iter() + .filter(|element_id| !entities.contains_key(element_id)) + .collect::>() + }; + + let weak_entities = Rc::downgrade(&self.entities); + for element_id in missing_entity_ids { + let entity = cx.new(|_| RandomizedElementEntity { + element_id, + snapshot: self.snapshot.clone(), + entities: weak_entities.clone(), + work_counters: self.work_counters.clone(), + }); + self.entities.borrow_mut().insert(element_id, entity); + } + + let entity_count = self.entities.borrow().len(); + let mut work_counters = self.work_counters.borrow_mut(); + let additional_capacity = + entity_count.saturating_sub(work_counters.rendered_entity_ids.len()); + work_counters + .rendered_entity_ids + .reserve(additional_capacity); + } + + fn notify_owner(&self, owner_entity_id: Option, cx: &mut Context) { + let owner = + owner_entity_id.and_then(|element_id| self.entities.borrow().get(&element_id).cloned()); + if let Some(owner) = owner { + owner.update(cx, |_, cx| cx.notify()); + } else { + cx.notify(); + } + } +} + +impl Render for RandomizedElementTree { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let snapshot = self.snapshot.borrow(); + let root = &snapshot.root; + let entities = self.entities.borrow(); + let mut work_counters = self.work_counters.borrow_mut(); + work_counters.root_render_count += 1; + + div() + .flex() + .when(root.vertical, |element| element.flex_col()) + .when(root.wrap, |element| element.flex_wrap()) + .gap(px(root.gap)) + .p(px(root.padding)) + .w(px(root.width)) + .h(px(root.height)) + .bg(root.background) + .border(px(root.border_width)) + .border_color(root.border_color) + .rounded(px(root.corner_radius)) + .opacity(root.opacity) + .when(root.clipped, |element| element.overflow_hidden()) + .when(!root.visible, |element| element.invisible()) + .children( + snapshot + .children + .iter() + .map(|node| render_node(node, &entities, &mut work_counters)), + ) + } +} + +type RandomizedElementEntities = HashMap>; + +struct RandomizedElementEntity { + element_id: u64, + snapshot: Rc>, + entities: Weak>, + work_counters: Rc>, +} + +impl Render for RandomizedElementEntity { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let snapshot = self.snapshot.borrow(); + let entities = self.entities.upgrade(); + let entities = entities.as_ref().map(|entities| entities.borrow()); + let mut work_counters = self.work_counters.borrow_mut(); + work_counters.entity_render_count += 1; + work_counters.rendered_entity_ids.push(self.element_id); + + snapshot + .node(self.element_id) + .map(|node| render_node_body(node, entities.as_deref(), &mut work_counters)) + .unwrap_or_else(|| div().into_any_element()) + } +} + +struct AppliedRandomizedElementTreeMutation { + mutation: RandomizedElementTreeMutation, + owner_entity_id: Option, + synchronize_entities: bool, +} + +impl AppliedRandomizedElementTreeMutation { + fn root(mutation: RandomizedElementTreeMutation) -> Self { + Self { + mutation, + owner_entity_id: None, + synchronize_entities: false, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +struct RandomizedElementNode { + id: u64, + style: RandomizedElementStyle, + is_entity: bool, + features: RandomizedElementFeatures, + children: Vec, +} + +impl RandomizedElementNode { + fn random(id: u64, entity_density: f64, handler_density: f64, rng: &mut StdRng) -> Self { + Self { + id, + style: RandomizedElementStyle::random_child(rng), + is_entity: rng.random_bool(entity_density), + features: RandomizedElementFeatures::random(handler_density, rng), + children: Vec::new(), + } + } + + fn element_count(&self) -> usize { + let mut count = 0; + let mut stack = vec![self]; + while let Some(node) = stack.pop() { + count += 1; + stack.extend(node.children.iter()); + } + count + } +} + +#[derive(Clone, Debug, PartialEq)] +struct RandomizedElementFeatures { + action_handler: bool, + click_handler: bool, + key_context: bool, +} + +impl RandomizedElementFeatures { + fn random(handler_density: f64, rng: &mut StdRng) -> Self { + Self { + action_handler: rng.random_bool(handler_density), + click_handler: rng.random_bool(handler_density), + key_context: rng.random_bool(handler_density), + } + } + + fn handler_count(&self) -> usize { + usize::from(self.action_handler) + usize::from(self.click_handler) + } +} + +#[derive(Clone, Debug, PartialEq)] +struct RandomizedElementStyle { + background: Hsla, + border_color: Hsla, + opacity: f32, + border_width: f32, + corner_radius: f32, + width: f32, + height: f32, + padding: f32, + gap: f32, + clipped: bool, + visible: bool, + vertical: bool, + wrap: bool, +} + +impl RandomizedElementStyle { + fn random_root(rng: &mut StdRng) -> Self { + Self { + background: random_color(rng), + border_color: random_color(rng), + opacity: random_opacity(rng), + border_width: rng.random_range(0.0..4.0), + corner_radius: rng.random_range(0.0..18.0), + width: rng.random_range(520.0..980.0), + height: rng.random_range(360.0..760.0), + padding: rng.random_range(4.0..18.0), + gap: rng.random_range(2.0..14.0), + clipped: rng.random_bool(0.5), + visible: true, + vertical: rng.random_bool(0.5), + wrap: rng.random_bool(0.7), + } + } + + fn random_child(rng: &mut StdRng) -> Self { + Self { + background: random_color(rng), + border_color: random_color(rng), + opacity: random_opacity(rng), + border_width: rng.random_range(0.0..4.0), + corner_radius: rng.random_range(0.0..14.0), + width: rng.random_range(36.0..220.0), + height: rng.random_range(28.0..180.0), + padding: rng.random_range(1.0..10.0), + gap: rng.random_range(1.0..8.0), + clipped: rng.random_bool(0.3), + visible: rng.random_bool(0.9), + vertical: rng.random_bool(0.5), + wrap: rng.random_bool(0.4), + } + } + + fn randomize_border(&mut self, rng: &mut StdRng) { + self.border_color = random_color(rng); + self.border_width = rng.random_range(0.0..5.0); + self.corner_radius = rng.random_range(0.0..18.0); + } +} + +#[derive(Clone, Copy)] +enum RandomizedChildMutation { + Color, + Opacity, + Border, + Bounds, + Clipping, + Visibility, +} + +#[cfg_attr(feature = "stacker", stacksafe::stacksafe)] +fn render_node( + node: &RandomizedElementNode, + entities: &RandomizedElementEntities, + work_counters: &mut RandomizedElementTreeWorkCounters, +) -> AnyElement { + if node.is_entity + && let Some(entity) = entities.get(&node.id) + { + return entity.clone().into_any_element(); + } + + render_node_body(node, Some(entities), work_counters) +} + +#[cfg_attr(feature = "stacker", stacksafe::stacksafe)] +fn render_node_body( + node: &RandomizedElementNode, + entities: Option<&RandomizedElementEntities>, + work_counters: &mut RandomizedElementTreeWorkCounters, +) -> AnyElement { + work_counters.element_render_count += 1; + work_counters.handler_registration_count += node.features.handler_count(); + let style = &node.style; + div() + .id(ElementId::NamedInteger( + "randomized-element".into(), + node.id, + )) + .flex() + .when(style.vertical, |element| element.flex_col()) + .when(style.wrap, |element| element.flex_wrap()) + .gap(px(style.gap)) + .p(px(style.padding)) + .w(px(style.width)) + .h(px(style.height)) + .bg(style.background) + .border(px(style.border_width)) + .border_color(style.border_color) + .rounded(px(style.corner_radius)) + .opacity(style.opacity) + .when(style.clipped, |element| element.overflow_hidden()) + .when(!style.visible, |element| element.invisible()) + .when(node.features.action_handler, |element| { + element.on_action(|_: &RandomizedElementTreeAction, _window, _cx| {}) + }) + .when(node.features.click_handler, |element| { + element.on_click(|_event, _window, _cx| {}) + }) + .when(node.features.key_context, |element| { + element.key_context("RandomizedElementTree") + }) + .children(node.children.iter().map(|child| { + if let Some(entities) = entities { + render_node(child, entities, work_counters) + } else { + render_node_body(child, None, work_counters) + } + })) + .into_any_element() +} + +fn insert_node( + root_children: &mut Vec, + parent_path: &mut Vec, + node: RandomizedElementNode, +) -> (usize, Vec) { + let Some(children) = children_at_path_mut(root_children, parent_path) else { + parent_path.clear(); + let index = root_children.len(); + root_children.push(node); + return (index, vec![index]); + }; + + let index = children.len(); + children.push(node); + let mut inserted_path = parent_path.clone(); + inserted_path.push(index); + (index, inserted_path) +} + +fn random_parent_path(root_children: &[RandomizedElementNode], rng: &mut StdRng) -> Vec { + let paths = node_paths(root_children); + let index = rng.random_range(0..=paths.len()); + paths.get(index).cloned().unwrap_or_default() +} + +fn random_node_path( + root_children: &[RandomizedElementNode], + rng: &mut StdRng, +) -> Option> { + let paths = node_paths(root_children); + if paths.is_empty() { + None + } else { + paths.get(rng.random_range(0..paths.len())).cloned() + } +} + +fn random_leaf_path( + root_children: &[RandomizedElementNode], + rng: &mut StdRng, +) -> Option> { + let leaves = node_paths(root_children) + .into_iter() + .filter(|path| { + node_at_path(root_children, path).is_some_and(|node| node.children.is_empty()) + }) + .collect::>(); + if leaves.is_empty() { + None + } else { + leaves.get(rng.random_range(0..leaves.len())).cloned() + } +} + +fn parent_paths_with_multiple_children(root_children: &[RandomizedElementNode]) -> Vec> { + let mut parent_paths = Vec::new(); + if root_children.len() >= 2 { + parent_paths.push(Vec::new()); + } + parent_paths.extend(node_paths(root_children).into_iter().filter(|path| { + node_at_path(root_children, path).is_some_and(|node| node.children.len() >= 2) + })); + parent_paths +} + +fn node_paths(root_children: &[RandomizedElementNode]) -> Vec> { + let mut paths = Vec::new(); + let mut stack = root_children + .iter() + .enumerate() + .rev() + .map(|(index, node)| (vec![index], node)) + .collect::>(); + + while let Some((path, node)) = stack.pop() { + for (index, child) in node.children.iter().enumerate().rev() { + let mut child_path = path.clone(); + child_path.push(index); + stack.push((child_path, child)); + } + paths.push(path); + } + paths +} + +fn node_at_path<'a>( + root_children: &'a [RandomizedElementNode], + path: &[usize], +) -> Option<&'a RandomizedElementNode> { + let (index, parent_path) = path.split_last()?; + children_at_path(root_children, parent_path)?.get(*index) +} + +fn node_at_path_mut<'a>( + root_children: &'a mut Vec, + path: &[usize], +) -> Option<&'a mut RandomizedElementNode> { + let (index, parent_path) = path.split_last()?; + children_at_path_mut(root_children, parent_path)?.get_mut(*index) +} + +fn children_at_path<'a>( + mut children: &'a [RandomizedElementNode], + path: &[usize], +) -> Option<&'a [RandomizedElementNode]> { + for index in path { + children = &children.get(*index)?.children; + } + Some(children) +} + +fn children_at_path_mut<'a>( + mut children: &'a mut Vec, + path: &[usize], +) -> Option<&'a mut Vec> { + for index in path { + children = &mut children.get_mut(*index)?.children; + } + Some(children) +} + +fn nearest_entity_id(root_children: &[RandomizedElementNode], path: &[usize]) -> Option { + let mut children = root_children; + let mut nearest_entity_id = None; + for index in path { + let node = children.get(*index)?; + if node.is_entity { + nearest_entity_id = Some(node.id); + } + children = &node.children; + } + nearest_entity_id +} + +fn normalized_density(density: f64) -> f64 { + if density.is_nan() { + 0.0 + } else { + density.clamp(0.0, 1.0) + } +} + +fn random_color(rng: &mut StdRng) -> Hsla { + Hsla { + h: rng.random_range(0.0..1.0), + s: rng.random_range(0.25..0.9), + l: rng.random_range(0.18..0.82), + a: 1.0, + } +} + +fn random_opacity(rng: &mut StdRng) -> f32 { + rng.random_range(0.25..1.0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AnyWindowHandle, Background, BorderStyle, Bounds, ContentMask, Corners, Edges, Pixels, + ScaledPixels, Size, TestAppContext, size, + }; + use proptest::prelude::*; + use std::collections::BTreeSet; + + const ELEMENT_COUNT: usize = 64; + const MUTATION_COUNT: usize = 32; + const WINDOW_SIZE: Size = size(px(1200.0), px(900.0)); + + #[derive(Debug, PartialEq)] + struct SceneSnapshot { + quads: Vec, + element_ids: BTreeSet, + } + + impl SceneSnapshot { + fn from_window(window: &Window) -> Self { + Self { + quads: window + .rendered_frame + .scene + .quads + .iter() + .map(QuadSnapshot::from) + .collect(), + element_ids: window + .rendered_frame + .element_states + .keys() + .filter_map(|(global_id, _)| { + let ElementId::NamedInteger(name, element_id) = global_id.0.last()? else { + return None; + }; + (name.as_ref() == "randomized-element").then_some(*element_id) + }) + .collect(), + } + } + } + + #[derive(Debug, PartialEq)] + struct QuadSnapshot { + order: u32, + border_style: BorderStyle, + bounds: Bounds, + content_mask: ContentMask, + background: Background, + border_color: Hsla, + corner_radii: Corners, + border_widths: Edges, + } + + impl From<&crate::Quad> for QuadSnapshot { + fn from(quad: &crate::Quad) -> Self { + Self { + order: quad.order, + border_style: quad.border_style, + bounds: quad.bounds, + content_mask: quad.content_mask, + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii, + border_widths: quad.border_widths, + } + } + } + + #[gpui::test] + fn localized_recoloring_notifies_one_entity_until_its_subtree_is_exhausted( + cx: &mut TestAppContext, + ) { + // Wide: every element is a root child, so every entity's subtree is exactly one + // element and localized change of k elements must touch k boundaries only when it + // spills; a mixed tree has larger subtrees to stay inside. + let config = RandomizedElementTreeConfig::new(11, 200) + .with_topology(RandomizedElementTreeTopology::Mixed) + .with_entity_density(0.1); + let window = cx.open_window(WINDOW_SIZE, move |_, cx| { + RandomizedElementTree::new_with_config(config, cx) + }); + let root = window.root(cx).expect("window has a root"); + let (localized, spread) = root.update(cx, |tree, cx| { + let localized = tree.recolor_children(20, ChangeLocality::Localized, cx); + let spread = tree.recolor_children(20, ChangeLocality::Spread, cx); + (localized, spread) + }); + assert_eq!(localized.recolored, 20); + assert_eq!(spread.recolored, 20); + assert!( + localized.notified <= spread.notified, + "localized change touches no more boundaries than spread change: {localized:?} vs {spread:?}" + ); + assert!(localized.notified >= 1); + } + + #[test] + fn bounds_sample_within_range_and_reproduce_from_the_seed() { + let bounds = RandomizedElementTreeBounds::new(32..=256) + .with_topologies([ + RandomizedElementTreeTopology::Deep, + RandomizedElementTreeTopology::Narrow, + ]) + .with_entity_density(0.1..=0.5) + .with_handler_density(0.25..=0.25); + let mut topologies = BTreeSet::new(); + let sample = |seed| bounds.sample(&mut StdRng::seed_from_u64(seed)); + for seed in 0..64 { + let config = sample(seed); + assert!((32..=256).contains(&config.element_count()), "{config:?}"); + assert!(config.topology().is_tall(), "{config:?}"); + assert!((0.1..=0.5).contains(&config.entity_density()), "{config:?}"); + assert_eq!(config.handler_density(), 0.25); + assert_eq!(sample(seed), config, "a seed draws the same config twice"); + topologies.insert(format!("{:?}", config.topology())); + } + assert_eq!(topologies.len(), 2, "64 seeds pick both allowed topologies"); + assert_ne!(sample(0), sample(1)); + } + + #[test] + fn generated_topologies_include_wide_deep_narrow_and_empty_trees() { + let wide = RandomizedElementTree::new(0, ELEMENT_COUNT).snapshot(); + assert_eq!(wide.children.len(), ELEMENT_COUNT); + assert_eq!(wide.max_depth(), 1); + + let deep = RandomizedElementTree::new(1, ELEMENT_COUNT).snapshot(); + assert_eq!(deep.children.len(), 1); + assert_eq!(deep.max_depth(), ELEMENT_COUNT); + + let narrow = RandomizedElementTree::new(3, ELEMENT_COUNT).snapshot(); + assert_eq!(narrow.descendant_count(), ELEMENT_COUNT); + assert!(narrow.max_depth() > 1); + + let empty = RandomizedElementTree::new(2, 0).snapshot(); + assert_eq!(empty.descendant_count(), 0); + assert_eq!(empty.max_depth(), 0); + } + + #[test] + fn randomized_element_ids_are_unique_and_stable() { + let mut tree = RandomizedElementTree::new(0, ELEMENT_COUNT); + let initial_ids = tree.snapshot().element_ids(); + assert_eq!(initial_ids.len(), ELEMENT_COUNT); + assert_eq!( + initial_ids.iter().copied().collect::>().len(), + ELEMENT_COUNT + ); + + for mutation in [ + RandomizedChildMutation::Color, + RandomizedChildMutation::Opacity, + RandomizedChildMutation::Border, + RandomizedChildMutation::Bounds, + RandomizedChildMutation::Clipping, + RandomizedChildMutation::Visibility, + ] { + tree.mutate_random_child(mutation); + assert_eq!(tree.snapshot().element_ids(), initial_ids); + } + + tree.reorder_child(); + let reordered_ids = tree.snapshot().element_ids(); + assert_eq!( + reordered_ids.iter().copied().collect::>(), + initial_ids.iter().copied().collect() + ); + + tree.insert_child(); + let inserted_ids = tree.snapshot().element_ids(); + let inserted_id_set = inserted_ids.iter().copied().collect::>(); + assert_eq!(inserted_ids.len(), ELEMENT_COUNT + 1); + assert!(initial_ids.iter().all(|id| inserted_id_set.contains(id))); + + tree.remove_child(); + let remaining_ids = tree.snapshot().element_ids(); + assert_eq!( + remaining_ids.iter().copied().collect::>().len(), + remaining_ids.len() + ); + assert!(remaining_ids.iter().all(|id| inserted_id_set.contains(id))); + } + + #[gpui::test] + fn randomized_entities_and_work_counters_support_incremental_benchmarks( + cx: &mut TestAppContext, + ) { + let config = RandomizedElementTreeConfig::new(0, 16) + .with_topology(RandomizedElementTreeTopology::Wide) + .with_entity_density(1.0) + .with_handler_density(1.0); + let window = cx.open_window(WINDOW_SIZE, move |_, cx| { + RandomizedElementTree::new_with_config(config, cx) + }); + let root = window + .root(cx) + .expect("randomized tree window should have a root"); + let window = AnyWindowHandle::from(window); + + draw_window(window, cx); + let initial_counters = root.read_with(cx, |tree, _| tree.work_counters()); + assert!(initial_counters.root_render_count() >= 1); + assert!(initial_counters.entity_render_count() >= 16); + assert!(initial_counters.element_render_count() >= 16); + assert!(initial_counters.handler_registration_count() >= 32); + + let mutation = root.update(cx, |tree, cx| { + tree.reset_work_counters(); + tree.apply_mutation(RandomizedElementTreeMutationKind::ChildColor, cx) + }); + let RandomizedElementTreeMutation::ChildColor { element_id } = mutation else { + panic!("a nonempty tree should apply the requested child-color mutation"); + }; + draw_window(window, cx); + + let incremental_counters = root.read_with(cx, |tree, _| tree.work_counters()); + assert!(incremental_counters.root_render_count() >= 1); + assert!(incremental_counters.entity_render_count() >= 1); + assert!(incremental_counters.element_render_count() >= 1); + assert!(incremental_counters.handler_registration_count() >= 2); + assert!( + incremental_counters + .rendered_entity_ids() + .contains(&element_id) + ); + + cx.update_window(window, |_, window, _| window.remove_window()) + .expect("randomized tree window should remain open until the case completes"); + } + + #[gpui::property_test(config = ProptestConfig { + cases: 4, + ..Default::default() + })] + fn randomized_element_tree_matches_full_render(cx: &mut TestAppContext, seed: u64) { + for topology in 0..4 { + let topology_seed = (seed & !3) | topology; + let config = RandomizedElementTreeConfig::new(topology_seed, ELEMENT_COUNT) + .with_entity_density(0.2) + .with_handler_density(0.2); + run_render_equivalence_case(cx, config); + } + run_render_equivalence_case( + cx, + RandomizedElementTreeConfig::new(seed, 0) + .with_entity_density(0.2) + .with_handler_density(0.2), + ); + } + + fn run_render_equivalence_case(cx: &mut TestAppContext, config: RandomizedElementTreeConfig) { + let persistent_window = cx.open_window(WINDOW_SIZE, move |_, cx| { + RandomizedElementTree::new_with_config(config, cx) + }); + let persistent_root = persistent_window + .root(cx) + .expect("randomized tree window should have a root"); + let persistent_window = AnyWindowHandle::from(persistent_window); + + assert_matches_reference(&persistent_root, persistent_window, cx, config, None); + + for mutation_index in 0..MUTATION_COUNT { + let mutation = persistent_root.update(cx, |tree, cx| match mutation_index { + 0 => tree.apply_mutation(RandomizedElementTreeMutationKind::Insert, cx), + 1 => tree.apply_mutation(RandomizedElementTreeMutationKind::Remove, cx), + _ => tree.apply_random_mutation(cx), + }); + + assert_matches_reference( + &persistent_root, + persistent_window, + cx, + config, + Some((mutation_index, mutation)), + ); + } + + cx.update_window(persistent_window, |_, window, _| window.remove_window()) + .expect("persistent window should remain open until the case completes"); + } + + fn assert_matches_reference( + persistent_root: &crate::Entity, + persistent_window: AnyWindowHandle, + cx: &mut TestAppContext, + config: RandomizedElementTreeConfig, + mutation: Option<(usize, RandomizedElementTreeMutation)>, + ) { + let snapshot = persistent_root.read_with(cx, |tree, _| tree.snapshot()); + let expected_element_ids = snapshot.element_ids().into_iter().collect::>(); + let persistent_scene = draw_window(persistent_window, cx); + assert_eq!( + persistent_scene.element_ids, expected_element_ids, + "persistent frame element IDs diverged from the model for config {config:?}, after \ + mutation {mutation:?}" + ); + let reference_scene = draw_fresh_reference(snapshot, cx); + + assert_eq!( + persistent_scene, reference_scene, + "rendered scene diverged for config {config:?}, after mutation {mutation:?}" + ); + } + + fn draw_fresh_reference( + snapshot: RandomizedElementTreeSnapshot, + cx: &mut TestAppContext, + ) -> SceneSnapshot { + let reference_window = cx.open_window(WINDOW_SIZE, move |_, cx| { + RandomizedElementTree::from_snapshot_with_entities(snapshot, cx) + }); + let reference_window = AnyWindowHandle::from(reference_window); + let scene = draw_window(reference_window, cx); + cx.update_window(reference_window, |_, window, _| window.remove_window()) + .expect("reference window should remain open until its frame is captured"); + scene + } + + fn draw_window(window: AnyWindowHandle, cx: &mut TestAppContext) -> SceneSnapshot { + cx.update_window(window, |_, window, cx| { + let arena_clear_needed = window.draw(cx); + let scene = SceneSnapshot::from_window(window); + arena_clear_needed.clear(cx); + scene + }) + .expect("randomized element tree window should remain open while drawing") + } +} diff --git a/vendor/gpui-pre/src/scene.rs b/vendor/gpui-pre/src/scene.rs new file mode 100644 index 0000000..160587a --- /dev/null +++ b/vendor/gpui-pre/src/scene.rs @@ -0,0 +1,949 @@ +// todo("windows"): remove +#![cfg_attr(windows, allow(dead_code))] + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, + Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, +}; +use std::{ + fmt::Debug, + iter::Peekable, + ops::{Add, Range, Sub}, + slice, +}; + +#[allow(non_camel_case_types, unused)] +#[expect(missing_docs)] +pub type PathVertex_ScaledPixels = PathVertex; + +#[expect(missing_docs)] +pub type DrawOrder = u32; + +/// A boolean stored as a `u32` so that GPU-facing structs contain no +/// compiler-inserted padding bytes, which would be undefined behavior to +/// reinterpret as `&[u8]` when writing instance buffers. Guaranteed to be +/// `0` or `1` by construction; shaders read it as a `u32`/`uint`. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[repr(transparent)] +pub struct PaddedBool32(u32); + +impl From for PaddedBool32 { + fn from(value: bool) -> Self { + PaddedBool32(value as u32) + } +} + +#[derive(Default)] +#[expect(missing_docs)] +pub struct Scene { + pub(crate) paint_operations: Vec, + primitive_bounds: BoundsTree, + layer_stack: Vec, + pub shadows: Vec, + pub quads: Vec, + pub paths: Vec>, + pub underlines: Vec, + pub monochrome_sprites: Vec, + pub subpixel_sprites: Vec, + pub polychrome_sprites: Vec, + pub surfaces: Vec, +} + +#[expect(missing_docs)] +impl Scene { + pub fn clear(&mut self) { + self.paint_operations.clear(); + self.primitive_bounds.clear(); + self.layer_stack.clear(); + self.paths.clear(); + self.shadows.clear(); + self.quads.clear(); + self.underlines.clear(); + self.monochrome_sprites.clear(); + self.subpixel_sprites.clear(); + self.polychrome_sprites.clear(); + self.surfaces.clear(); + } + + pub fn len(&self) -> usize { + self.paint_operations.len() + } + + pub fn push_layer(&mut self, bounds: Bounds) { + let order = self.primitive_bounds.insert(bounds); + self.layer_stack.push(order); + self.paint_operations + .push(PaintOperation::StartLayer(bounds)); + } + + pub fn pop_layer(&mut self) { + self.layer_stack.pop(); + self.paint_operations.push(PaintOperation::EndLayer); + } + + pub fn insert_primitive(&mut self, primitive: impl Into) { + let mut primitive = primitive.into(); + let clipped_bounds = primitive + .bounds() + .intersect(&primitive.content_mask().bounds); + + if clipped_bounds.is_empty() { + return; + } + + let order = self + .layer_stack + .last() + .copied() + .unwrap_or_else(|| self.primitive_bounds.insert(clipped_bounds)); + match &mut primitive { + Primitive::Shadow(shadow) => { + shadow.order = order; + self.shadows.push(*shadow); + } + Primitive::Quad(quad) => { + quad.order = order; + self.quads.push(*quad); + } + Primitive::Path(path) => { + path.order = order; + path.id = PathId(self.paths.len()); + self.paths.push(path.clone()); + } + Primitive::Underline(underline) => { + underline.order = order; + self.underlines.push(*underline); + } + Primitive::MonochromeSprite(sprite) => { + sprite.order = order; + self.monochrome_sprites.push(*sprite); + } + Primitive::SubpixelSprite(sprite) => { + sprite.order = order; + self.subpixel_sprites.push(*sprite); + } + Primitive::PolychromeSprite(sprite) => { + sprite.order = order; + self.polychrome_sprites.push(*sprite); + } + Primitive::Surface(surface) => { + surface.order = order; + self.surfaces.push(surface.clone()); + } + } + self.paint_operations + .push(PaintOperation::Primitive(primitive)); + } + + pub fn replay(&mut self, range: Range, prev_scene: &Scene) { + for operation in &prev_scene.paint_operations[range] { + match operation { + PaintOperation::Primitive(primitive) => self.insert_primitive(primitive.clone()), + PaintOperation::StartLayer(bounds) => self.push_layer(*bounds), + PaintOperation::EndLayer => self.pop_layer(), + } + } + } + + pub fn finish(&mut self) { + self.shadows.sort_by_key(|shadow| shadow.order); + self.quads.sort_by_key(|quad| quad.order); + self.paths.sort_by_key(|path| path.order); + self.underlines.sort_by_key(|underline| underline.order); + self.monochrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.subpixel_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.polychrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.surfaces.sort_by_key(|surface| surface.order); + } + + #[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) + )] + pub fn batches(&self) -> impl Iterator + '_ { + BatchIterator { + shadows_start: 0, + shadows_iter: self.shadows.iter().peekable(), + quads_start: 0, + quads_iter: self.quads.iter().peekable(), + paths_start: 0, + paths_iter: self.paths.iter().peekable(), + underlines_start: 0, + underlines_iter: self.underlines.iter().peekable(), + monochrome_sprites_start: 0, + monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(), + subpixel_sprites_start: 0, + subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(), + polychrome_sprites_start: 0, + polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(), + surfaces_start: 0, + surfaces_iter: self.surfaces.iter().peekable(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +pub(crate) enum PrimitiveKind { + Shadow, + #[default] + Quad, + Path, + Underline, + MonochromeSprite, + SubpixelSprite, + PolychromeSprite, + Surface, +} + +pub(crate) enum PaintOperation { + Primitive(Primitive), + StartLayer(Bounds), + EndLayer, +} + +#[derive(Clone)] +#[expect(missing_docs)] +pub enum Primitive { + Shadow(Shadow), + Quad(Quad), + Path(Path), + Underline(Underline), + MonochromeSprite(MonochromeSprite), + SubpixelSprite(SubpixelSprite), + PolychromeSprite(PolychromeSprite), + Surface(PaintSurface), +} + +#[expect(missing_docs)] +impl Primitive { + pub fn bounds(&self) -> &Bounds { + match self { + Primitive::Shadow(shadow) => &shadow.bounds, + Primitive::Quad(quad) => &quad.bounds, + Primitive::Path(path) => &path.bounds, + Primitive::Underline(underline) => &underline.bounds, + Primitive::MonochromeSprite(sprite) => &sprite.bounds, + Primitive::SubpixelSprite(sprite) => &sprite.bounds, + Primitive::PolychromeSprite(sprite) => &sprite.bounds, + Primitive::Surface(surface) => &surface.bounds, + } + } + + pub fn content_mask(&self) -> &ContentMask { + match self { + Primitive::Shadow(shadow) => &shadow.content_mask, + Primitive::Quad(quad) => &quad.content_mask, + Primitive::Path(path) => &path.content_mask, + Primitive::Underline(underline) => &underline.content_mask, + Primitive::MonochromeSprite(sprite) => &sprite.content_mask, + Primitive::SubpixelSprite(sprite) => &sprite.content_mask, + Primitive::PolychromeSprite(sprite) => &sprite.content_mask, + Primitive::Surface(surface) => &surface.content_mask, + } + } +} + +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +struct BatchIterator<'a> { + shadows_start: usize, + shadows_iter: Peekable>, + quads_start: usize, + quads_iter: Peekable>, + paths_start: usize, + paths_iter: Peekable>>, + underlines_start: usize, + underlines_iter: Peekable>, + monochrome_sprites_start: usize, + monochrome_sprites_iter: Peekable>, + subpixel_sprites_start: usize, + subpixel_sprites_iter: Peekable>, + polychrome_sprites_start: usize, + polychrome_sprites_iter: Peekable>, + surfaces_start: usize, + surfaces_iter: Peekable>, +} + +impl<'a> Iterator for BatchIterator<'a> { + type Item = PrimitiveBatch; + + fn next(&mut self) -> Option { + let mut orders_and_kinds = [ + ( + self.shadows_iter.peek().map(|s| s.order), + PrimitiveKind::Shadow, + ), + (self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad), + (self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path), + ( + self.underlines_iter.peek().map(|u| u.order), + PrimitiveKind::Underline, + ), + ( + self.monochrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::MonochromeSprite, + ), + ( + self.subpixel_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::SubpixelSprite, + ), + ( + self.polychrome_sprites_iter.peek().map(|s| s.order), + PrimitiveKind::PolychromeSprite, + ), + ( + self.surfaces_iter.peek().map(|s| s.order), + PrimitiveKind::Surface, + ), + ]; + orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind)); + + let first = orders_and_kinds[0]; + let second = orders_and_kinds[1]; + let (batch_kind, max_order_and_kind) = if first.0.is_some() { + (first.1, (second.0.unwrap_or(u32::MAX), second.1)) + } else { + return None; + }; + + match batch_kind { + PrimitiveKind::Shadow => { + let shadows_start = self.shadows_start; + let mut shadows_end = shadows_start + 1; + self.shadows_iter.next(); + while self + .shadows_iter + .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) + .is_some() + { + shadows_end += 1; + } + self.shadows_start = shadows_end; + Some(PrimitiveBatch::Shadows(shadows_start..shadows_end)) + } + PrimitiveKind::Quad => { + let quads_start = self.quads_start; + let mut quads_end = quads_start + 1; + self.quads_iter.next(); + while self + .quads_iter + .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) + .is_some() + { + quads_end += 1; + } + self.quads_start = quads_end; + Some(PrimitiveBatch::Quads(quads_start..quads_end)) + } + PrimitiveKind::Path => { + let paths_start = self.paths_start; + let mut paths_end = paths_start + 1; + self.paths_iter.next(); + while self + .paths_iter + .next_if(|path| (path.order, batch_kind) < max_order_and_kind) + .is_some() + { + paths_end += 1; + } + self.paths_start = paths_end; + Some(PrimitiveBatch::Paths(paths_start..paths_end)) + } + PrimitiveKind::Underline => { + let underlines_start = self.underlines_start; + let mut underlines_end = underlines_start + 1; + self.underlines_iter.next(); + while self + .underlines_iter + .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) + .is_some() + { + underlines_end += 1; + } + self.underlines_start = underlines_end; + Some(PrimitiveBatch::Underlines(underlines_start..underlines_end)) + } + PrimitiveKind::MonochromeSprite => { + let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.monochrome_sprites_start; + let mut sprites_end = sprites_start + 1; + self.monochrome_sprites_iter.next(); + while self + .monochrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.monochrome_sprites_start = sprites_end; + Some(PrimitiveBatch::MonochromeSprites { + texture_id, + range: sprites_start..sprites_end, + }) + } + PrimitiveKind::SubpixelSprite => { + let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.subpixel_sprites_start; + let mut sprites_end = sprites_start + 1; + self.subpixel_sprites_iter.next(); + while self + .subpixel_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.subpixel_sprites_start = sprites_end; + Some(PrimitiveBatch::SubpixelSprites { + texture_id, + range: sprites_start..sprites_end, + }) + } + PrimitiveKind::PolychromeSprite => { + let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id; + let sprites_start = self.polychrome_sprites_start; + let mut sprites_end = sprites_start + 1; + self.polychrome_sprites_iter.next(); + while self + .polychrome_sprites_iter + .next_if(|sprite| { + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id + }) + .is_some() + { + sprites_end += 1; + } + self.polychrome_sprites_start = sprites_end; + Some(PrimitiveBatch::PolychromeSprites { + texture_id, + range: sprites_start..sprites_end, + }) + } + PrimitiveKind::Surface => { + let surfaces_start = self.surfaces_start; + let mut surfaces_end = surfaces_start + 1; + self.surfaces_iter.next(); + while self + .surfaces_iter + .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) + .is_some() + { + surfaces_end += 1; + } + self.surfaces_start = surfaces_end; + Some(PrimitiveBatch::Surfaces(surfaces_start..surfaces_end)) + } + } + } +} + +#[derive(Debug)] +#[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) +)] +#[allow(missing_docs)] +pub enum PrimitiveBatch { + Shadows(Range), + Quads(Range), + Paths(Range), + Underlines(Range), + MonochromeSprites { + texture_id: AtlasTextureId, + range: Range, + }, + #[cfg_attr(target_os = "macos", allow(dead_code))] + SubpixelSprites { + texture_id: AtlasTextureId, + range: Range, + }, + PolychromeSprites { + texture_id: AtlasTextureId, + range: Range, + }, + Surfaces(Range), +} + +impl PrimitiveBatch { + #[expect(missing_docs)] + pub fn label(&self) -> String { + match self { + Self::Shadows(range) => format!("shadows ({})", range.len()), + Self::Quads(range) => format!("quads ({})", range.len()), + Self::Paths(range) => format!("paths ({})", range.len()), + Self::Underlines(range) => format!("underlines ({})", range.len()), + Self::MonochromeSprites { texture_id, range } => { + format!( + "monochrome sprites ({}) on atlas {}", + range.len(), + texture_id.index + ) + } + Self::SubpixelSprites { texture_id, range } => { + format!( + "subpixel sprites ({}) on atlas {}", + range.len(), + texture_id.index + ) + } + Self::PolychromeSprites { texture_id, range } => { + format!( + "polychrome sprites ({}) on atlas {}", + range.len(), + texture_id.index + ) + } + Self::Surfaces(range) => format!("surfaces ({})", range.len()), + } + } +} + +#[derive(Default, Debug, Copy, Clone)] +#[repr(C)] +#[expect(missing_docs)] +pub struct Quad { + pub order: DrawOrder, + pub border_style: BorderStyle, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub background: Background, + pub border_color: Hsla, + pub corner_radii: Corners, + pub border_widths: Edges, +} + +impl From for Primitive { + fn from(quad: Quad) -> Self { + Primitive::Quad(quad) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +#[expect(missing_docs)] +pub struct Underline { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub thickness: ScaledPixels, + pub wavy: PaddedBool32, +} + +impl From for Primitive { + fn from(underline: Underline) -> Self { + Primitive::Underline(underline) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(C)] +#[expect(missing_docs)] +pub struct Shadow { + pub order: DrawOrder, + pub blur_radius: ScaledPixels, + pub bounds: Bounds, + pub corner_radii: Corners, + pub content_mask: ContentMask, + pub color: Hsla, + pub element_bounds: Bounds, + pub element_corner_radii: Corners, + /// 0 = drop shadow (rendered outside the element), 1 = inset shadow (rendered inside). + pub inset: u32, + pub pad: u32, // align to 8 bytes +} + +impl From for Primitive { + fn from(shadow: Shadow) -> Self { + Primitive::Shadow(shadow) + } +} + +/// The style of a border. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[repr(C)] +pub enum BorderStyle { + /// A solid border. + #[default] + Solid = 0, + /// A dashed border. + Dashed = 1, +} + +/// A data type representing a 2 dimensional transformation that can be applied to an element. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct TransformationMatrix { + /// 2x2 matrix containing rotation and scale, + /// stored row-major + pub rotation_scale: [[f32; 2]; 2], + /// translation vector + pub translation: [f32; 2], +} + +impl Eq for TransformationMatrix {} + +impl TransformationMatrix { + /// The unit matrix, has no effect. + pub fn unit() -> Self { + Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [0.0, 0.0], + } + } + + /// Move the origin by a given point + pub fn translate(mut self, point: Point) -> Self { + self.compose(Self { + rotation_scale: [[1.0, 0.0], [0.0, 1.0]], + translation: [point.x.0, point.y.0], + }) + } + + /// Clockwise rotation in radians around the origin + pub fn rotate(self, angle: Radians) -> Self { + self.compose(Self { + rotation_scale: [ + [angle.0.cos(), -angle.0.sin()], + [angle.0.sin(), angle.0.cos()], + ], + translation: [0.0, 0.0], + }) + } + + /// Scale around the origin + pub fn scale(self, size: Size) -> Self { + self.compose(Self { + rotation_scale: [[size.width, 0.0], [0.0, size.height]], + translation: [0.0, 0.0], + }) + } + + /// Perform matrix multiplication with another transformation + /// to produce a new transformation that is the result of + /// applying both transformations: first, `other`, then `self`. + #[inline] + pub fn compose(self, other: TransformationMatrix) -> TransformationMatrix { + if other == Self::unit() { + return self; + } + // Perform matrix multiplication + TransformationMatrix { + rotation_scale: [ + [ + self.rotation_scale[0][0] * other.rotation_scale[0][0] + + self.rotation_scale[0][1] * other.rotation_scale[1][0], + self.rotation_scale[0][0] * other.rotation_scale[0][1] + + self.rotation_scale[0][1] * other.rotation_scale[1][1], + ], + [ + self.rotation_scale[1][0] * other.rotation_scale[0][0] + + self.rotation_scale[1][1] * other.rotation_scale[1][0], + self.rotation_scale[1][0] * other.rotation_scale[0][1] + + self.rotation_scale[1][1] * other.rotation_scale[1][1], + ], + ], + translation: [ + self.translation[0] + + self.rotation_scale[0][0] * other.translation[0] + + self.rotation_scale[0][1] * other.translation[1], + self.translation[1] + + self.rotation_scale[1][0] * other.translation[0] + + self.rotation_scale[1][1] * other.translation[1], + ], + } + } + + /// Apply transformation to a point, mainly useful for debugging + pub fn apply(&self, point: Point) -> Point { + let input = [point.x.0, point.y.0]; + let mut output = self.translation; + for (i, output_cell) in output.iter_mut().enumerate() { + for (k, input_cell) in input.iter().enumerate() { + *output_cell += self.rotation_scale[i][k] * *input_cell; + } + } + Point::new(output[0].into(), output[1].into()) + } +} + +impl Default for TransformationMatrix { + fn default() -> Self { + Self::unit() + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +#[expect(missing_docs)] +pub struct MonochromeSprite { + pub order: DrawOrder, + pub pad: u32, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub tile: AtlasTile, + pub transformation: TransformationMatrix, +} + +impl From for Primitive { + fn from(sprite: MonochromeSprite) -> Self { + Primitive::MonochromeSprite(sprite) + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +#[expect(missing_docs)] +pub struct SubpixelSprite { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub tile: AtlasTile, + pub transformation: TransformationMatrix, +} + +impl From for Primitive { + fn from(sprite: SubpixelSprite) -> Self { + Primitive::SubpixelSprite(sprite) + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(C)] +#[expect(missing_docs)] +pub struct PolychromeSprite { + pub order: DrawOrder, + pub pad: u32, + pub grayscale: PaddedBool32, + pub opacity: f32, + pub bounds: Bounds, + pub content_mask: ContentMask, + pub corner_radii: Corners, + pub tile: AtlasTile, +} + +impl From for Primitive { + fn from(sprite: PolychromeSprite) -> Self { + Primitive::PolychromeSprite(sprite) + } +} + +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub struct PaintSurface { + pub order: DrawOrder, + pub bounds: Bounds, + pub content_mask: ContentMask, + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub image_buffer: core_video::pixel_buffer::CVPixelBuffer, +} + +impl From for Primitive { + fn from(surface: PaintSurface) -> Self { + Primitive::Surface(surface) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[expect(missing_docs)] +pub struct PathId(pub usize); + +/// A line made up of a series of vertices and control points. +#[derive(Clone, Debug)] +#[expect(missing_docs)] +pub struct Path { + pub id: PathId, + pub order: DrawOrder, + pub bounds: Bounds

, + pub content_mask: ContentMask

, + pub vertices: Vec>, + pub color: Background, + start: Point

, + current: Point

, + contour_count: usize, +} + +impl Path { + /// Create a new path with the given starting point. + pub fn new(start: Point) -> Self { + Self { + id: PathId(0), + order: DrawOrder::default(), + vertices: Vec::new(), + start, + current: start, + bounds: Bounds { + origin: start, + size: Default::default(), + }, + content_mask: Default::default(), + color: Default::default(), + contour_count: 0, + } + } + + /// Scale this path by the given factor. + pub fn scale(&self, factor: f32) -> Path { + Path { + id: self.id, + order: self.order, + bounds: self.bounds.scale(factor), + content_mask: self.content_mask.scale(factor), + vertices: self + .vertices + .iter() + .map(|vertex| vertex.scale(factor)) + .collect(), + start: self.start.map(|start| start.scale(factor)), + current: self.current.scale(factor), + contour_count: self.contour_count, + color: self.color, + } + } + + /// Move the start, current point to the given point. + pub fn move_to(&mut self, to: Point) { + self.contour_count += 1; + self.start = to; + self.current = to; + } + + /// Draw a straight line from the current point to the given point. + pub fn line_to(&mut self, to: Point) { + self.contour_count += 1; + if self.contour_count > 1 { + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + self.current = to; + } + + /// Draw a curve from the current point to the given point, using the given control point. + pub fn curve_to(&mut self, to: Point, ctrl: Point) { + self.contour_count += 1; + if self.contour_count > 1 { + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); + } + + self.push_triangle( + (self.current, ctrl, to), + (point(0., 0.), point(0.5, 0.), point(1., 1.)), + ); + self.current = to; + } + + /// Push a triangle to the Path. + pub fn push_triangle( + &mut self, + xy: (Point, Point, Point), + st: (Point, Point, Point), + ) { + self.bounds = self + .bounds + .union(&Bounds { + origin: xy.0, + size: Default::default(), + }) + .union(&Bounds { + origin: xy.1, + size: Default::default(), + }) + .union(&Bounds { + origin: xy.2, + size: Default::default(), + }); + + self.vertices.push(PathVertex { + xy_position: xy.0, + st_position: st.0, + content_mask: Default::default(), + }); + self.vertices.push(PathVertex { + xy_position: xy.1, + st_position: st.1, + content_mask: Default::default(), + }); + self.vertices.push(PathVertex { + xy_position: xy.2, + st_position: st.2, + content_mask: Default::default(), + }); + } +} + +impl Path +where + T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, +{ + #[allow(unused)] + #[expect(missing_docs)] + pub fn clipped_bounds(&self) -> Bounds { + self.bounds.intersect(&self.content_mask.bounds) + } +} + +impl From> for Primitive { + fn from(path: Path) -> Self { + Primitive::Path(path) + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +#[expect(missing_docs)] +pub struct PathVertex { + pub xy_position: Point

, + pub st_position: Point, + pub content_mask: ContentMask

, +} + +#[expect(missing_docs)] +impl PathVertex { + pub fn scale(&self, factor: f32) -> PathVertex { + PathVertex { + xy_position: self.xy_position.scale(factor), + st_position: self.st_position, + content_mask: self.content_mask.scale(factor), + } + } +} diff --git a/vendor/gpui-pre/src/seeds.rs b/vendor/gpui-pre/src/seeds.rs new file mode 100644 index 0000000..e90784d --- /dev/null +++ b/vendor/gpui-pre/src/seeds.rs @@ -0,0 +1,67 @@ +use std::env; + +/// Returns the seeds a randomized `#[gpui::test]` or `#[gpui::bench]` runs with, in +/// order, and whether there is more than one. +/// +/// With `$SEED` set, runs `$SEED..$SEED + iterations` and ignores `explicit_seeds`. +/// Otherwise runs `0..iterations` followed by `explicit_seeds`, except that the +/// default single iteration is dropped when explicit seeds are given. +/// `$ITERATIONS` overrides `iterations`. +/// Tests and benchmarks share this so that one `SEED` reproduces either. +/// +/// This is intended for use by the `gpui::test` and `gpui::bench` macros and generally +/// should not be used directly. +#[doc(hidden)] +pub fn calculate_seeds( + iterations: u64, + explicit_seeds: &[u64], +) -> (impl Iterator + '_, bool) { + let iterations = env::var("ITERATIONS") + .ok() + .map(|var| var.parse().expect("invalid ITERATIONS variable")) + .unwrap_or(iterations); + + let env_seed = env::var("SEED") + .map(|seed| seed.parse().expect("invalid SEED variable as integer")) + .ok(); + + let iter = seeds(iterations, explicit_seeds, env_seed); + let is_multiple_runs = iter.clone().nth(1).is_some(); + (iter, is_multiple_runs) +} + +fn seeds( + iterations: u64, + explicit_seeds: &[u64], + env_seed: Option, +) -> impl Iterator + Clone + '_ { + let (iterations_range, explicit_seeds) = match env_seed { + Some(env_seed) => (env_seed..env_seed + iterations, &[][..]), + None if iterations == 1 && !explicit_seeds.is_empty() => (0..0, explicit_seeds), + None => (0..iterations, explicit_seeds), + }; + iterations_range.chain(explicit_seeds.iter().copied()) +} + +#[cfg(test)] +mod tests { + use super::seeds; + + fn collect(iterations: u64, explicit_seeds: &[u64], env_seed: Option) -> Vec { + seeds(iterations, explicit_seeds, env_seed).collect() + } + + #[test] + fn seeds_follow_iterations_explicit_seeds_and_seed_variable() { + assert_eq!(collect(1, &[], None), [0]); + assert_eq!(collect(3, &[], None), [0, 1, 2]); + assert_eq!(collect(1, &[10, 20], None), [10, 20]); + assert_eq!(collect(2, &[10], None), [0, 1, 10]); + assert_eq!(collect(1, &[10], Some(7)), [7]); + assert_eq!( + collect(2, &[10], Some(7)), + [7, 8], + "`SEED` starts the iterations once, without repeating itself" + ); + } +} diff --git a/vendor/gpui-pre/src/shared_uri.rs b/vendor/gpui-pre/src/shared_uri.rs new file mode 100644 index 0000000..e257aaf --- /dev/null +++ b/vendor/gpui-pre/src/shared_uri.rs @@ -0,0 +1,25 @@ +use derive_more::{Deref, DerefMut}; + +use crate::SharedString; + +/// A [`SharedString`] containing a URI. +#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)] +pub struct SharedUri(SharedString); + +impl std::fmt::Debug for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl> From for SharedUri { + fn from(value: T) -> Self { + Self(value.into()) + } +} diff --git a/vendor/gpui-pre/src/spring.rs b/vendor/gpui-pre/src/spring.rs new file mode 100644 index 0000000..efc8780 --- /dev/null +++ b/vendor/gpui-pre/src/spring.rs @@ -0,0 +1,830 @@ +use std::{ops::RangeInclusive, time::Duration}; + +use crate::{Hsla, Pixels, Rems, Rgba}; + +const CRITICAL_DAMPING_TOLERANCE: f32 = 1e-4; +const DEFAULT_SPRING_EPSILON: f32 = 0.001; + +/// The physical parameters of a damped harmonic oscillator. +/// +/// `stiffness` and `mass` must be finite and positive. `damping` must be finite +/// and non-negative. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SpringConfig { + /// The spring stiffness, conventionally written as $k$. + pub stiffness: f32, + /// The viscous damping coefficient, conventionally written as $c$. + pub damping: f32, + /// The moving mass, conventionally written as $m$. + pub mass: f32, +} + +impl SpringConfig { + /// Creates a spring from its physical parameters. + pub const fn new(stiffness: f32, damping: f32, mass: f32) -> Self { + Self { + stiffness, + damping, + mass, + } + } + + /// Returns the natural angular frequency and damping ratio $(\omega_0, \zeta)$. + pub fn canonical(&self) -> (f32, f32) { + let natural_frequency = (self.stiffness / self.mass).sqrt(); + let damping_ratio = self.damping / (2.0 * (self.stiffness * self.mass).sqrt()); + (natural_frequency, damping_ratio) + } + + /// Advances a spring toward a target that remains fixed for `delta_time`. + /// + /// This analytic step is independent of frame rate and preserves velocity, + /// allowing an interrupted spring to be retargeted without restarting it. + pub fn step(&self, state: SpringState, target: f32, delta_time: f32) -> SpringState { + let propagator = self.propagator(delta_time); + let displacement = state.position - target; + + SpringState { + position: target + propagator[0][0] * displacement + propagator[0][1] * state.velocity, + velocity: propagator[1][0] * displacement + propagator[1][1] * state.velocity, + } + } + + /// Advances a spring toward a target moving at a constant velocity. + /// + /// A first-order hold avoids the frame-rate-dependent lag introduced by + /// treating a dragged target as stationary between frames. + pub fn step_ramp( + &self, + state: SpringState, + target: f32, + target_velocity: f32, + delta_time: f32, + ) -> SpringState { + let (natural_frequency, damping_ratio) = self.canonical(); + let steady_state_lag = -2.0 * damping_ratio * target_velocity / natural_frequency; + let displacement = state.position - target - steady_state_lag; + let velocity = state.velocity - target_velocity; + let propagator = self.propagator(delta_time); + let target = target + target_velocity * delta_time; + + SpringState { + position: target + + steady_state_lag + + propagator[0][0] * displacement + + propagator[0][1] * velocity, + velocity: target_velocity + + propagator[1][0] * displacement + + propagator[1][1] * velocity, + } + } + + /// Returns the exact state-transition matrix for a constant target. + /// + /// Materializing this matrix is useful when many springs share the same + /// configuration and frame delta. A matrix must not be reused when the + /// frame delta changes. + pub fn propagator(&self, delta_time: f32) -> [[f32; 2]; 2] { + let (natural_frequency, damping_ratio) = self.canonical(); + + if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE { + let decay = damping_ratio * natural_frequency; + let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt(); + let exponential = (-decay * delta_time).exp(); + let (sine, cosine) = (damped_frequency * delta_time).sin_cos(); + let sine_over_frequency = sine / damped_frequency; + + [ + [ + exponential * (cosine + decay * sine_over_frequency), + exponential * sine_over_frequency, + ], + [ + -exponential * natural_frequency * natural_frequency * sine_over_frequency, + exponential * (cosine - decay * sine_over_frequency), + ], + ] + } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE { + let root = (damping_ratio * damping_ratio - 1.0).sqrt(); + let root_sum = damping_ratio + root; + let slow_root = -natural_frequency / root_sum; + let fast_root = -natural_frequency * root_sum; + let denominator = slow_root - fast_root; + let slow_exponential = (slow_root * delta_time).exp(); + let fast_exponential = (fast_root * delta_time).exp(); + + [ + [ + (-fast_root * slow_exponential + slow_root * fast_exponential) / denominator, + (slow_exponential - fast_exponential) / denominator, + ], + [ + slow_root * fast_root * (fast_exponential - slow_exponential) / denominator, + (slow_root * slow_exponential - fast_root * fast_exponential) / denominator, + ], + ] + } else { + let exponential = (-natural_frequency * delta_time).exp(); + + [ + [ + exponential * (1.0 + natural_frequency * delta_time), + exponential * delta_time, + ], + [ + -exponential * natural_frequency * natural_frequency * delta_time, + exponential * (1.0 - natural_frequency * delta_time), + ], + ] + } + } + + /// Tests both displacement and velocity against a positional tolerance. + /// + /// Velocity is compared with `epsilon * natural_frequency`, giving it the + /// corresponding animated-units-per-second scale. + pub fn is_settled(&self, state: SpringState, target: f32, epsilon: f32) -> bool { + let (natural_frequency, _) = self.canonical(); + epsilon.is_finite() + && epsilon >= 0.0 + && (state.position - target).abs() <= epsilon + && state.velocity.abs() <= epsilon * natural_frequency + } + + /// Returns a conservative time after which the spring remains settled. + /// + /// An undamped spring has no finite settling time and returns + /// [`Duration::MAX`]. + pub fn settle_time(&self, state: SpringState, target: f32, epsilon: f32) -> Duration { + let displacement = state.position - target; + if displacement == 0.0 && state.velocity == 0.0 { + return Duration::ZERO; + } + + let (natural_frequency, damping_ratio) = self.canonical(); + if !natural_frequency.is_finite() + || natural_frequency <= 0.0 + || !damping_ratio.is_finite() + || damping_ratio <= 0.0 + || !epsilon.is_finite() + || epsilon <= 0.0 + { + return Duration::MAX; + } + + let velocity_threshold = epsilon * natural_frequency; + + if damping_ratio < 1.0 - CRITICAL_DAMPING_TOLERANCE { + let decay = damping_ratio * natural_frequency; + let damped_frequency = natural_frequency * (1.0 - damping_ratio * damping_ratio).sqrt(); + let sine_coefficient = (state.velocity + decay * displacement) / damped_frequency; + let position_envelope = displacement.hypot(sine_coefficient); + let velocity_cosine = damped_frequency * sine_coefficient - decay * displacement; + let velocity_sine = -damped_frequency * displacement - decay * sine_coefficient; + let velocity_envelope = velocity_cosine.hypot(velocity_sine); + + find_settle_time( + epsilon, + velocity_threshold, + 0.0, + natural_frequency, + move |time| { + let exponential = (-decay * time).exp(); + ( + position_envelope * exponential, + velocity_envelope * exponential, + ) + }, + ) + } else if damping_ratio > 1.0 + CRITICAL_DAMPING_TOLERANCE { + let root = (damping_ratio * damping_ratio - 1.0).sqrt(); + let root_sum = damping_ratio + root; + let slow_root = -natural_frequency / root_sum; + let fast_root = -natural_frequency * root_sum; + let denominator = slow_root - fast_root; + let slow_coefficient = (state.velocity - fast_root * displacement) / denominator; + let fast_coefficient = (slow_root * displacement - state.velocity) / denominator; + + find_settle_time( + epsilon, + velocity_threshold, + 0.0, + natural_frequency, + move |time| { + let slow_term = slow_coefficient.abs() * (slow_root * time).exp(); + let fast_term = fast_coefficient.abs() * (fast_root * time).exp(); + ( + slow_term + fast_term, + slow_root.abs() * slow_term + fast_root.abs() * fast_term, + ) + }, + ) + } else { + let linear_coefficient = state.velocity + natural_frequency * displacement; + let position_constant = displacement.abs(); + let position_linear = linear_coefficient.abs(); + let velocity_constant = (linear_coefficient - natural_frequency * displacement).abs(); + let velocity_linear = natural_frequency * linear_coefficient.abs(); + let position_decay_start = + envelope_decay_start(position_constant, position_linear, natural_frequency); + let velocity_decay_start = + envelope_decay_start(velocity_constant, velocity_linear, natural_frequency); + + find_settle_time( + epsilon, + velocity_threshold, + position_decay_start.max(velocity_decay_start), + natural_frequency, + move |time| { + let exponential = (-natural_frequency * time).exp(); + ( + (position_constant + position_linear * time) * exponential, + (velocity_constant + velocity_linear * time) * exponential, + ) + }, + ) + } + } +} + +/// The instantaneous position and velocity of a spring. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct SpringState { + /// The current value in the animated unit. + pub position: f32, + /// The current value's change per second. + pub velocity: f32, +} + +/// A value that can be targeted by a one-dimensional spring. +/// +/// Implementations may project the spring coordinate into a richer output, +/// allowing a discrete state or a path through a multidimensional value to be +/// driven by one spring. +pub trait SpringTarget: 'static { + /// The value supplied to the spring animator. + type Output; + + /// Returns the target in the spring's coordinate space. + fn target(&self) -> f32; + + /// Projects a spring coordinate into the animated output. + fn resolve(&self, value: f32) -> Self::Output; +} + +impl SpringTarget for f32 { + type Output = f32; + + fn target(&self) -> f32 { + *self + } + + fn resolve(&self, value: f32) -> Self::Output { + value + } +} + +impl SpringTarget for Pixels { + type Output = Pixels; + + fn target(&self) -> f32 { + self.as_f32() + } + + fn resolve(&self, value: f32) -> Self::Output { + Pixels::from(value) + } +} + +impl SpringTarget for Rems { + type Output = Rems; + + fn target(&self) -> f32 { + self.0 + } + + fn resolve(&self, value: f32) -> Self::Output { + Rems(value) + } +} + +impl SpringTarget for bool { + type Output = AnimationPhase; + + fn target(&self) -> f32 { + if *self { 1.0 } else { 0.0 } + } + + fn resolve(&self, value: f32) -> Self::Output { + AnimationPhase(value) + } +} + +/// A potentially overshooting coordinate within an animation. +/// +/// Phases are not restricted to 0..1. Multi-stage animations can assign each +/// stage its own coordinate and interpolate over ranges such as 1..=2. +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)] +pub struct AnimationPhase( + /// The unbounded phase coordinate. + pub f32, +); + +impl AnimationPhase { + /// Restricts this phase to the bounds of a range. + pub fn clamp(self, range: RangeInclusive) -> Self { + let (first, second) = range.into_inner(); + Self(self.0.clamp(first.min(second), first.max(second))) + } + + /// Interpolates between values using 0 and 1 as their phase coordinates. + pub fn interpolate(self, from: T, to: T) -> T { + T::interpolate(from, to, self.0) + } + + /// Interpolates between values without extrapolating beyond 0 and 1. + pub fn interpolate_clamped(self, from: T, to: T) -> T { + T::interpolate(from, to, self.0.clamp(0.0, 1.0)) + } + + /// Interpolates between values assigned to arbitrary phase coordinates. + pub fn interpolate_between( + self, + range: RangeInclusive, + from: T, + to: T, + ) -> T { + let (start, end) = range.into_inner(); + let phase = if start == end { + if self.0 < start { 0.0 } else { 1.0 } + } else { + (self.0 - start) / (end - start) + }; + T::interpolate(from, to, phase) + } + + /// Interpolates over arbitrary phase coordinates without extrapolating. + pub fn interpolate_between_clamped( + self, + range: RangeInclusive, + from: T, + to: T, + ) -> T { + let (start, end) = range.into_inner(); + let phase = if start == end { + if self.0 < start { 0.0 } else { 1.0 } + } else { + ((self.0 - start) / (end - start)).clamp(0.0, 1.0) + }; + T::interpolate(from, to, phase) + } +} + +impl From for AnimationPhase { + fn from(value: f32) -> Self { + Self(value) + } +} + +impl From for AnimationPhase { + fn from(value: bool) -> Self { + Self(if value { 1.0 } else { 0.0 }) + } +} + +impl SpringTarget for AnimationPhase { + type Output = AnimationPhase; + + fn target(&self) -> f32 { + self.0 + } + + fn resolve(&self, value: f32) -> Self::Output { + Self(value) + } +} + +/// A value that supports linear interpolation and extrapolation. +pub trait Interpolate: Sized { + /// Resolves the value at `phase`, where 0 is `from` and 1 is `to`. + fn interpolate(from: Self, to: Self, phase: f32) -> Self; +} + +impl Interpolate for f32 { + fn interpolate(from: Self, to: Self, phase: f32) -> Self { + from + (to - from) * phase + } +} + +impl Interpolate for Pixels { + fn interpolate(from: Self, to: Self, phase: f32) -> Self { + from + (to - from) * phase + } +} + +impl Interpolate for Rems { + fn interpolate(from: Self, to: Self, phase: f32) -> Self { + from + (to - from) * phase + } +} + +impl Interpolate for Rgba { + fn interpolate(from: Self, to: Self, phase: f32) -> Self { + Self { + r: f32::interpolate(from.r, to.r, phase), + g: f32::interpolate(from.g, to.g, phase), + b: f32::interpolate(from.b, to.b, phase), + a: f32::interpolate(from.a, to.a, phase), + } + } +} + +impl Interpolate for Hsla { + fn interpolate(from: Self, to: Self, phase: f32) -> Self { + let hue_delta = (to.h - from.h + 0.5).rem_euclid(1.0) - 0.5; + Self { + h: (from.h + hue_delta * phase).rem_euclid(1.0), + s: f32::interpolate(from.s, to.s, phase), + l: f32::interpolate(from.l, to.l, phase), + a: f32::interpolate(from.a, to.a, phase), + } + } +} + +/// Controls how a spring advances and resolves its presentation value. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SpringPlayback { + /// Advances toward the latest target, preserving velocity across retargets. + #[default] + Running, + /// Holds the current position and velocity until playback resumes. + Paused, + /// Holds the current position and discards velocity. + Stopped, + /// Snaps to the latest target and discards velocity. + Completed, + /// Returns to the initial value and discards velocity. + Cancelled, +} + +/// A stateful spring animation targeting a value or projected path. +#[derive(Clone, Debug)] +pub struct SpringAnimation { + pub(crate) config: SpringConfig, + pub(crate) target: T, + pub(crate) epsilon: f32, + pub(crate) initial: Option, + pub(crate) playback: SpringPlayback, +} + +impl SpringAnimation<()> { + /// Creates a spring animation builder. + pub fn new(config: SpringConfig) -> Self { + Self { + config, + target: (), + epsilon: DEFAULT_SPRING_EPSILON, + initial: None, + playback: SpringPlayback::Running, + } + } + + /// Sets the value or path targeted by this spring. + pub fn to(self, target: T) -> SpringAnimation { + let SpringAnimation { + config, + target: (), + epsilon, + initial, + playback, + } = self; + SpringAnimation { + config, + target, + epsilon, + initial, + playback, + } + } +} + +impl SpringAnimation { + /// Sets the settling tolerance in the target's scalar coordinate space. + pub fn with_epsilon(mut self, epsilon: f32) -> Self { + self.epsilon = epsilon; + self + } + + /// Sets how the spring advances or resolves its current value. + pub fn playback(mut self, playback: SpringPlayback) -> Self { + self.playback = playback; + self + } +} + +impl SpringAnimation { + /// Sets the coordinate used when this element has no prior spring state. + pub fn from(mut self, initial: T) -> Self { + self.initial = Some(initial.target()); + self + } +} + +/// Adapts a spring starting at zero with no velocity to GPUI's duration-based easing API. +/// +/// The returned easing can overshoot the normalized 0..1 output range. Retargeting +/// this duration-based form restarts the spring; use [`SpringConfig::step`] when +/// preserving velocity matters. +pub fn sampled_easing(config: SpringConfig, epsilon: f32) -> (Duration, impl Fn(f32) -> f32) { + let initial_state = SpringState { + position: 0.0, + velocity: 0.0, + }; + let duration = config.settle_time(initial_state, 1.0, epsilon); + let duration_seconds = duration.as_secs_f32(); + + (duration, move |progress| { + if progress <= 0.0 { + 0.0 + } else if progress >= 1.0 { + 1.0 + } else { + config + .step(initial_state, 1.0, progress * duration_seconds) + .position + } + }) +} + +fn envelope_decay_start(constant: f32, linear: f32, decay: f32) -> f32 { + if linear == 0.0 { + 0.0 + } else { + (1.0 / decay - constant / linear).max(0.0) + } +} + +fn find_settle_time( + position_threshold: f32, + velocity_threshold: f32, + decay_start: f32, + natural_frequency: f32, + envelope: impl Fn(f32) -> (f32, f32), +) -> Duration { + let is_below_threshold = |time| { + let (position, velocity) = envelope(time); + position <= position_threshold && velocity <= velocity_threshold + }; + + if is_below_threshold(decay_start) { + return duration_from_secs(decay_start); + } + + let mut lower_bound = decay_start; + let mut upper_bound = decay_start.max(natural_frequency.recip()); + while !is_below_threshold(upper_bound) { + lower_bound = upper_bound; + upper_bound *= 2.0; + if !upper_bound.is_finite() { + return Duration::MAX; + } + } + + for _ in 0..32 { + let midpoint = (lower_bound + upper_bound) / 2.0; + if is_below_threshold(midpoint) { + upper_bound = midpoint; + } else { + lower_bound = midpoint; + } + } + + duration_from_secs(upper_bound) +} + +fn duration_from_secs(seconds: f32) -> Duration { + if !seconds.is_finite() || seconds >= Duration::MAX.as_secs_f32() { + Duration::MAX + } else if seconds <= 0.0 { + Duration::ZERO + } else { + Duration::from_secs_f32(seconds) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EPSILON: f32 = 1e-4; + + #[test] + fn spring_targets_resolve_typed_outputs() { + assert_eq!(12.0_f32.target(), 12.0); + assert_eq!(12.0_f32.resolve(14.0), 14.0); + assert_eq!(Pixels::from(12.0).target(), 12.0); + assert_eq!(Pixels::from(12.0).resolve(14.0), Pixels::from(14.0)); + assert_eq!(false.target(), 0.0); + assert_eq!(true.target(), 1.0); + assert_eq!(true.resolve(1.25), AnimationPhase(1.25)); + } + + #[test] + fn animation_phases_interpolate_over_arbitrary_ranges() { + let phase = AnimationPhase(1.5); + assert_eq!(phase.interpolate_between(1.0..=2.0, 10.0, 20.0), 15.0); + assert_eq!( + phase.interpolate_between_clamped(2.0..=3.0, 10.0, 20.0), + 10.0 + ); + assert_eq!( + AnimationPhase(3.5).interpolate_between(2.0..=3.0, 10.0, 20.0), + 25.0 + ); + } + + #[test] + fn hsla_interpolation_takes_the_shortest_hue_path() { + let from = Hsla { + h: 0.9, + s: 0.5, + l: 0.5, + a: 1.0, + }; + let to = Hsla { + h: 0.1, + s: 1.0, + l: 0.75, + a: 0.5, + }; + let result = AnimationPhase(0.5).interpolate(from, to); + + assert!(result.h < EPSILON || (1.0 - result.h) < EPSILON); + assert!((result.s - 0.75).abs() < EPSILON); + assert!((result.l - 0.625).abs() < EPSILON); + assert!((result.a - 0.75).abs() < EPSILON); + } + + #[test] + fn propagators_compose_and_have_expected_determinant() { + for damping_ratio in [0.4, 1.0, 1.5] { + let natural_frequency = 12.0; + let config = SpringConfig::new( + natural_frequency * natural_frequency, + 2.0 * damping_ratio * natural_frequency, + 1.0, + ); + let first = config.propagator(0.013); + let second = config.propagator(0.021); + let combined = multiply(second, first); + let direct = config.propagator(0.034); + + for row in 0..2 { + for column in 0..2 { + assert!( + (combined[row][column] - direct[row][column]).abs() < 2e-4, + "{damping_ratio}: {combined:?} != {direct:?}" + ); + } + } + + let determinant = direct[0][0] * direct[1][1] - direct[0][1] * direct[1][0]; + let expected = (-2.0 * damping_ratio * natural_frequency * 0.034).exp(); + assert!((determinant - expected).abs() < 2e-4); + } + } + + #[test] + fn step_preserves_semigroup_for_every_damping_regime() { + let state = SpringState { + position: -3.0, + velocity: 5.0, + }; + for damping in [4.0, 20.0, 40.0] { + let config = SpringConfig::new(100.0, damping, 1.0); + let stepped = config.step(config.step(state, 7.0, 0.013), 7.0, 0.021); + let direct = config.step(state, 7.0, 0.034); + + assert!((stepped.position - direct.position).abs() < 2e-4); + assert!((stepped.velocity - direct.velocity).abs() < 2e-4); + } + } + + #[test] + fn ramp_tracks_steady_state_lag() { + let natural_frequency = 10.0; + let damping_ratio = 0.8; + let target_velocity = 3.0; + let config = SpringConfig::new( + natural_frequency * natural_frequency, + 2.0 * damping_ratio * natural_frequency, + 1.0, + ); + let lag = -2.0 * damping_ratio * target_velocity / natural_frequency; + let state = SpringState { + position: lag, + velocity: target_velocity, + }; + let next = config.step_ramp(state, 0.0, target_velocity, 0.25); + + assert!((next.position - (target_velocity * 0.25 + lag)).abs() < EPSILON); + assert!((next.velocity - target_velocity).abs() < EPSILON); + } + + #[test] + fn settling_requires_low_velocity() { + let config = SpringConfig::new(100.0, 10.0, 1.0); + assert!(!config.is_settled( + SpringState { + position: 1.0, + velocity: 1.0, + }, + 1.0, + 0.01, + )); + assert!(config.is_settled( + SpringState { + position: 1.005, + velocity: 0.05, + }, + 1.0, + 0.01, + )); + } + + #[test] + fn settle_time_is_conservative_for_every_damping_regime() { + let initial_state = SpringState { + position: -2.0, + velocity: 4.0, + }; + for damping in [4.0, 20.0, 40.0] { + let config = SpringConfig::new(100.0, damping, 1.0); + let duration = config.settle_time(initial_state, 3.0, 0.001); + assert_ne!(duration, Duration::MAX); + + for additional_time in [0.0, 0.1, 1.0] { + let state = + config.step(initial_state, 3.0, duration.as_secs_f32() + additional_time); + assert!( + config.is_settled(state, 3.0, 0.001), + "{damping}: {duration:?} produced {state:?}" + ); + } + } + } + + #[test] + fn settle_time_accounts_for_motion_outside_an_instantaneous_tolerance() { + let config = SpringConfig::new(100.0, 2.0, 1.0); + let state = SpringState { + position: 1.125, + velocity: 1.25, + }; + assert!(config.is_settled(state, 1.0, 0.125)); + + let duration = config.settle_time(state, 1.0, 0.125); + assert!(duration > Duration::ZERO); + assert!(config.is_settled(config.step(state, 1.0, duration.as_secs_f32()), 1.0, 0.125,)); + } + + #[test] + fn undamped_spring_never_settles() { + let config = SpringConfig::new(100.0, 0.0, 1.0); + assert_eq!( + config.settle_time( + SpringState { + position: 0.0, + velocity: 0.0, + }, + 1.0, + 0.001, + ), + Duration::MAX + ); + } + + #[test] + fn sampled_easing_has_exact_endpoints_and_can_overshoot() { + let config = SpringConfig::new(100.0, 6.0, 1.0); + let (duration, easing) = sampled_easing(config, 0.001); + + assert_ne!(duration, Duration::MAX); + assert_eq!(easing(0.0), 0.0); + assert_eq!(easing(1.0), 1.0); + assert!((1..100).any(|step| easing(step as f32 / 100.0) > 1.0)); + } + + fn multiply(left: [[f32; 2]; 2], right: [[f32; 2]; 2]) -> [[f32; 2]; 2] { + [ + [ + left[0][0] * right[0][0] + left[0][1] * right[1][0], + left[0][0] * right[0][1] + left[0][1] * right[1][1], + ], + [ + left[1][0] * right[0][0] + left[1][1] * right[1][0], + left[1][0] * right[0][1] + left[1][1] * right[1][1], + ], + ] + } +} diff --git a/vendor/gpui-pre/src/style.rs b/vendor/gpui-pre/src/style.rs new file mode 100644 index 0000000..9150fdd --- /dev/null +++ b/vendor/gpui-pre/src/style.rs @@ -0,0 +1,1529 @@ +use std::{ + hash::{Hash, Hasher}, + iter, mem, + ops::Range, +}; + +use crate::{ + AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, + CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, + FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, + PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi, + point, px, quad, rems, size, +}; +use collections::HashSet; +use refineable::Refineable; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Use this struct for interfacing with the 'debug_below' styling from your own elements. +/// If a parent element has this style set on it, then this struct will be set as a global in +/// GPUI. +#[cfg(debug_assertions)] +pub struct DebugBelow; + +#[cfg(debug_assertions)] +impl crate::Global for DebugBelow {} + +/// How to fit the image into the bounds of the element. +pub enum ObjectFit { + /// The image will be stretched to fill the bounds of the element. + Fill, + /// The image will be scaled to fit within the bounds of the element. + Contain, + /// The image will be scaled to cover the bounds of the element. + Cover, + /// The image will be scaled down to fit within the bounds of the element. + ScaleDown, + /// The image will maintain its original size. + None, +} + +impl ObjectFit { + /// Get the bounds of the image within the given bounds. + pub fn get_bounds( + &self, + bounds: Bounds, + image_size: Size, + ) -> Bounds { + let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension))); + let image_ratio = image_size.width / image_size.height; + let bounds_ratio = bounds.size.width / bounds.size.height; + + match self { + ObjectFit::Fill => bounds, + ObjectFit::Contain => { + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::ScaleDown => { + // Check if the image is larger than the bounds in either dimension. + if image_size.width > bounds.size.width || image_size.height > bounds.size.height { + // If the image is larger, use the same logic as Contain to scale it down. + let new_size = if bounds_ratio > image_ratio { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + } else { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } else { + // If the image is smaller than or equal to the container, display it at its original size, + // centered within the container. + let original_size = size(image_size.width, image_size.height); + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - original_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - original_size.height) / 2.0, + ), + size: original_size, + } + } + } + ObjectFit::Cover => { + let new_size = if bounds_ratio > image_ratio { + size( + bounds.size.width, + image_size.height * (bounds.size.width / image_size.width), + ) + } else { + size( + image_size.width * (bounds.size.height / image_size.height), + bounds.size.height, + ) + }; + + Bounds { + origin: point( + bounds.origin.x + (bounds.size.width - new_size.width) / 2.0, + bounds.origin.y + (bounds.size.height - new_size.height) / 2.0, + ), + size: new_size, + } + } + ObjectFit::None => Bounds { + origin: bounds.origin, + size: image_size, + }, + } + } +} + +/// The minimum size of a column or row in a grid layout +#[derive( + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize, +)] +pub enum GridTemplateMinSize { + /// The column or row size may be 0 + #[default] + Zero, + /// The column or row size can be determined by the min content + MinContent, + /// The column or row size can be determined by the max content + MaxContent, +} + +/// A simplified representation of the grid-template-* value +#[derive( + Copy, + Clone, + Refineable, + PartialEq, + Eq, + PartialOrd, + Ord, + Debug, + Default, + JsonSchema, + Serialize, + Deserialize, +)] +pub struct GridTemplate { + /// How this template directive should be repeated + pub repeat: u16, + /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation + pub min_size: GridTemplateMinSize, +} + +/// The CSS styling that can be applied to an element via the `Styled` trait +#[derive(Clone, Refineable, Debug)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct Style { + /// What layout strategy should be used? + pub display: Display, + + /// Should the element be painted on screen? + pub visibility: Visibility, + + // Overflow properties + /// How children overflowing their container should affect layout + #[refineable] + pub overflow: Point, + /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. + pub scrollbar_width: AbsoluteLength, + /// Whether both x and y axis should be scrollable at the same time. + pub allow_concurrent_scroll: bool, + /// Whether scrolling should be restricted to the input gesture's axis. + /// + /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be + /// released when the gesture changes direction strongly. Touch phases delimit gestures when + /// available, with a timeout fallback for platforms that only emit moved events. + /// + /// This also prevents input from being remapped to another axis. For example, horizontal input + /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms + /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis. + /// + /// ## Motivation + /// + /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when + /// the mouse is over a horizontally-scrollable element. + /// + /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis + /// to the X axis. + /// + /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis + /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains + /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be + /// hijacked. + /// + /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in + /// style property to limit the potential blast radius. + pub restrict_scroll_to_axis: bool, + + // Position properties + /// What should the `position` value of this struct use as a base offset? + pub position: Position, + /// How should the position of this element be tweaked relative to the layout defined? + #[refineable] + pub inset: Edges, + + // Size properties + /// Sets the initial size of the item + #[refineable] + pub size: Size, + /// Controls the minimum size of the item + #[refineable] + pub min_size: Size, + /// Controls the maximum size of the item + #[refineable] + pub max_size: Size, + /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height. + pub aspect_ratio: Option, + + // Spacing Properties + /// How large should the margin be on each side? + #[refineable] + pub margin: Edges, + /// How large should the padding be on each side? + #[refineable] + pub padding: Edges, + /// How large should the border be on each side? + #[refineable] + pub border_widths: Edges, + + // Alignment properties + /// How this node's children aligned in the cross/block axis? + pub align_items: Option, + /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set + pub align_self: Option, + /// How should content contained within this item be aligned in the cross/block axis + pub align_content: Option, + /// How should contained within this item be aligned in the main/inline axis + pub justify_content: Option, + /// How large should the gaps between items in a flex container be? + #[refineable] + pub gap: Size, + + // Flexbox properties + /// Which direction does the main axis flow in? + pub flex_direction: FlexDirection, + /// Should elements wrap, or stay in a single line? + pub flex_wrap: FlexWrap, + /// Sets the initial main axis size of the item + pub flex_basis: Length, + /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive. + pub flex_grow: f32, + /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive. + pub flex_shrink: f32, + + /// The fill color of this element + pub background: Option, + + /// The border color of this element + pub border_color: Option, + + /// The border style of this element + pub border_style: BorderStyle, + + /// The radius of the corners of this element + #[refineable] + pub corner_radii: Corners, + + /// Box shadow of the element + pub box_shadow: Vec, + + /// The text style of this element + #[refineable] + pub text: TextStyleRefinement, + + /// The mouse cursor style shown when the mouse pointer is over an element. + pub mouse_cursor: Option, + + /// The opacity of this element + pub opacity: Option, + + /// The grid columns of this element + /// Roughly equivalent to the Tailwind `grid-cols-` + pub grid_cols: Option, + + /// The row span of this element + /// Equivalent to the Tailwind `grid-rows-` + pub grid_rows: Option, + + /// The grid location of this element + pub grid_location: Option, + + /// Whether to draw a red debugging outline around this element + #[cfg(debug_assertions)] + pub debug: bool, + + /// Whether to draw a red debugging outline around this element and all of its conforming children + #[cfg(debug_assertions)] + pub debug_below: bool, +} + +impl Styled for StyleRefinement { + fn style(&mut self) -> &mut StyleRefinement { + self + } +} + +impl StyleRefinement { + /// The grid location of this element + pub fn grid_location_mut(&mut self) -> &mut GridLocation { + self.grid_location.get_or_insert_default() + } +} + +/// The value of the visibility property, similar to the CSS property `visibility` +#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum Visibility { + /// The element should be drawn as normal. + #[default] + Visible, + /// The element should not be drawn, but should still take up space in the layout. + Hidden, +} + +/// The possible values of the box-shadow property +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct BoxShadow { + /// What color should the shadow have? + pub color: Hsla, + /// How should it be offset from its element? + pub offset: Point, + /// How much should the shadow be blurred? + pub blur_radius: Pixels, + /// How much should the shadow spread? + pub spread_radius: Pixels, + /// Whether this is an inset shadow (drawn inside the element's bounds). + pub inset: bool, +} + +impl BoxShadow { + /// Creates a new [`BoxShadow`] with the given offset and color, matching the order + /// of the CSS `box-shadow` property. Use the builder methods to set blur radius, + /// spread radius, and inset. + pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self { + Self { + color, + offset: point(offset_x, offset_y), + blur_radius: px(0.), + spread_radius: px(0.), + inset: false, + } + } + + /// Sets the shadow blur radius. + pub fn blur_radius(mut self, blur_radius: Pixels) -> Self { + self.blur_radius = blur_radius; + self + } + + /// Sets the shadow spread radius. + pub fn spread_radius(mut self, spread_radius: Pixels) -> Self { + self.spread_radius = spread_radius; + self + } + + /// Marks the shadow as inset (drawn inside the element's bounds). + pub fn inset(mut self) -> Self { + self.inset = true; + self + } +} + +/// How to handle whitespace in text +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum WhiteSpace { + /// Normal line wrapping when text overflows the width of the element + #[default] + Normal, + /// No line wrapping, text will overflow the width of the element + Nowrap, +} + +/// How to truncate text that overflows the width of the element +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum TextOverflow { + /// Truncate the text at the end when it doesn't fit, and represent this truncation by + /// displaying the provided string (e.g., "very long te…"). + Truncate(SharedString), + /// Truncate the text at the start when it doesn't fit, and represent this truncation by + /// displaying the provided string at the beginning (e.g., "…ong text here"). + /// Typically more adequate for file paths where the end is more important than the beginning. + TruncateStart(SharedString), + /// Truncate the text in the middle when it doesn't fit, preserving both the start and end + /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix + /// and the extension are important context. + TruncateMiddle(SharedString), +} + +/// How to align text within the element +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum TextAlign { + /// Align the text to the left of the element + #[default] + Left, + + /// Center the text within the element + Center, + + /// Align the text to the right of the element + Right, +} + +/// The properties that can be used to style text in GPUI +#[derive(Refineable, Clone, Debug, PartialEq)] +#[refineable(Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct TextStyle { + /// The color of the text + pub color: Hsla, + + /// The font family to use + pub font_family: SharedString, + + /// The font features to use + pub font_features: FontFeatures, + + /// The fallback fonts to use + pub font_fallbacks: Option, + + /// The font size to use, in pixels or rems. + pub font_size: AbsoluteLength, + + /// The line height to use, in pixels or fractions + pub line_height: DefiniteLength, + + /// The font weight, e.g. bold + pub font_weight: FontWeight, + + /// The font style, e.g. italic + pub font_style: FontStyle, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The strikethrough style of the text + pub strikethrough: Option, + + /// How to handle whitespace in the text + pub white_space: WhiteSpace, + + /// The text should be truncated if it overflows the width of the element + pub text_overflow: Option, + + /// How the text should be aligned within the element + pub text_align: TextAlign, + + /// The number of lines to display before truncating the text + pub line_clamp: Option, +} + +impl Default for TextStyle { + fn default() -> Self { + TextStyle { + color: black(), + // todo(linux) make this configurable or choose better default + font_family: ".SystemUIFont".into(), + font_features: FontFeatures::default(), + font_fallbacks: None, + font_size: rems(1.).into(), + line_height: phi(), + font_weight: FontWeight::default(), + font_style: FontStyle::default(), + background_color: None, + underline: None, + strikethrough: None, + white_space: WhiteSpace::Normal, + text_overflow: None, + text_align: TextAlign::default(), + line_clamp: None, + } + } +} + +impl TextStyle { + /// Create a new text style with the given highlighting applied. + pub fn highlight(mut self, style: impl Into) -> Self { + let style = style.into(); + if let Some(weight) = style.font_weight { + self.font_weight = weight; + } + if let Some(style) = style.font_style { + self.font_style = style; + } + + if let Some(color) = style.color { + self.color = self.color.blend(color); + } + + if let Some(factor) = style.fade_out { + self.color.fade_out(factor); + } + + if let Some(background_color) = style.background_color { + self.background_color = Some(background_color); + } + + if let Some(underline) = style.underline { + self.underline = Some(underline); + } + + if let Some(strikethrough) = style.strikethrough { + self.strikethrough = Some(strikethrough); + } + + self + } + + /// Get the font configured for this text style. + pub fn font(&self) -> Font { + Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + fallbacks: self.font_fallbacks.clone(), + weight: self.font_weight, + style: self.font_style, + } + } + + /// Returns the rounded line height in pixels. + pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels { + self.line_height.to_pixels(self.font_size, rem_size).round() + } + + /// Convert this text style into a [`TextRun`], for the given length of the text. + pub fn to_run(&self, len: usize) -> TextRun { + TextRun { + len, + font: Font { + family: self.font_family.clone(), + features: self.font_features.clone(), + fallbacks: self.font_fallbacks.clone(), + weight: self.font_weight, + style: self.font_style, + }, + color: self.color, + background_color: self.background_color, + underline: self.underline, + strikethrough: self.strikethrough, + } + } +} + +/// A highlight style to apply, similar to a `TextStyle` except +/// for a single font, uniformly sized and spaced text. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct HighlightStyle { + /// The color of the text + pub color: Option, + + /// The font weight, e.g. bold + pub font_weight: Option, + + /// The font style, e.g. italic + pub font_style: Option, + + /// The background color of the text + pub background_color: Option, + + /// The underline style of the text + pub underline: Option, + + /// The underline style of the text + pub strikethrough: Option, + + /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant. + pub fade_out: Option, +} + +impl Eq for HighlightStyle {} + +impl Hash for HighlightStyle { + fn hash(&self, state: &mut H) { + self.color.hash(state); + self.font_weight.hash(state); + self.font_style.hash(state); + self.background_color.hash(state); + self.underline.hash(state); + self.strikethrough.hash(state); + state.write_u32(u32::from_be_bytes( + self.fade_out.map(|f| f.to_be_bytes()).unwrap_or_default(), + )); + } +} + +impl Style { + /// Returns true if the style is visible and the background is opaque. + pub fn has_opaque_background(&self) -> bool { + self.background + .as_ref() + .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + } + + /// Get the text style in this element style. + pub fn text_style(&self) -> Option<&TextStyleRefinement> { + if self.text.is_some() { + Some(&self.text) + } else { + None + } + } + + /// Get the content mask for this element style, based on the given bounds. + /// If the element does not hide its overflow, this will return `None`. + pub fn overflow_mask( + &self, + bounds: Bounds, + rem_size: Pixels, + ) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let mut min = bounds.origin; + let mut max = bounds.bottom_right(); + + if self + .border_color + .is_some_and(|color| !color.is_transparent()) + { + min.x += self.border_widths.left.to_pixels(rem_size); + max.x -= self.border_widths.right.to_pixels(rem_size); + min.y += self.border_widths.top.to_pixels(rem_size); + max.y -= self.border_widths.bottom.to_pixels(rem_size); + } + + let bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return None, + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.bottom_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.bottom_right().x, max.y), + ), + // both hidden + (false, false) => Bounds::from_corners(min, max), + }; + + Some(ContentMask { bounds }) + } + } + } + + /// Paints the background of an element styled with this style. + pub fn paint( + &self, + bounds: Bounds, + window: &mut Window, + cx: &mut App, + continuation: impl FnOnce(&mut Window, &mut App), + ) { + #[cfg(debug_assertions)] + if self.debug_below { + cx.set_global(DebugBelow) + } + + #[cfg(debug_assertions)] + if self.debug || cx.has_global::() { + window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default())); + } + + let rem_size = window.rem_size(); + let corner_radii = self + .corner_radii + .to_pixels(rem_size) + .clamp_radii_for_quad_size(bounds.size); + + window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow); + + let background_color = self.background.as_ref().and_then(Fill::color); + if background_color.is_some_and(|color| !color.is_transparent()) { + let mut border_color = match background_color { + Some(color) => match color.tag { + BackgroundTag::Solid + | BackgroundTag::PatternSlash + | BackgroundTag::Checkerboard => color.solid, + + BackgroundTag::LinearGradient => color + .colors + .first() + .map(|stop| stop.color) + .unwrap_or_default(), + }, + None => Hsla::default(), + }; + border_color.a = 0.; + window.paint_quad(quad( + bounds, + corner_radii, + background_color.unwrap_or_default(), + Edges::default(), + border_color, + self.border_style, + )); + } + + window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow); + + continuation(window, cx); + + if self.is_border_visible() { + let border_widths = self.border_widths.to_pixels(rem_size); + let mut background = self.border_color.unwrap_or_default(); + background.a = 0.; + window.paint_quad(quad( + bounds, + corner_radii, + background, + border_widths, + self.border_color.unwrap_or_default(), + self.border_style, + )); + } + + #[cfg(debug_assertions)] + if self.debug_below { + cx.remove_global::(); + } + } + + fn is_border_visible(&self) -> bool { + self.border_color + .is_some_and(|color| !color.is_transparent()) + && self.border_widths.any(|length| !length.is_zero()) + } +} + +impl Default for Style { + fn default() -> Self { + Style { + display: Display::Block, + visibility: Visibility::Visible, + overflow: Point { + x: Overflow::Visible, + y: Overflow::Visible, + }, + allow_concurrent_scroll: false, + restrict_scroll_to_axis: false, + scrollbar_width: AbsoluteLength::default(), + position: Position::Relative, + inset: Edges::auto(), + margin: Edges::::zero(), + padding: Edges::::zero(), + border_widths: Edges::::zero(), + size: Size::auto(), + min_size: Size::auto(), + max_size: Size::auto(), + aspect_ratio: None, + gap: Size::default(), + // Alignment + align_items: None, + align_self: None, + align_content: None, + justify_content: None, + // Flexbox + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::NoWrap, + flex_grow: 0.0, + flex_shrink: 1.0, + flex_basis: Length::Auto, + background: None, + border_color: None, + border_style: BorderStyle::default(), + corner_radii: Corners::default(), + box_shadow: Default::default(), + text: TextStyleRefinement::default(), + mouse_cursor: None, + opacity: None, + grid_rows: None, + grid_cols: None, + grid_location: None, + + #[cfg(debug_assertions)] + debug: false, + #[cfg(debug_assertions)] + debug_below: false, + } + } +} + +/// The properties that can be applied to an underline. +#[derive( + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, +)] +pub struct UnderlineStyle { + /// The thickness of the underline. + pub thickness: Pixels, + + /// The color of the underline. + pub color: Option, + + /// Whether the underline should be wavy, like in a spell checker. + pub wavy: bool, +} + +/// The properties that can be applied to a strikethrough. +#[derive( + Refineable, Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, +)] +pub struct StrikethroughStyle { + /// The thickness of the strikethrough. + pub thickness: Pixels, + + /// The color of the strikethrough. + pub color: Option, +} + +/// The kinds of fill that can be applied to a shape. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub enum Fill { + /// A solid color fill. + Color(Background), +} + +impl Fill { + /// Unwrap this fill into a solid color, if it is one. + /// + /// If the fill is not a solid color, this method returns `None`. + pub fn color(&self) -> Option { + match self { + Fill::Color(color) => Some(*color), + } + } +} + +impl Default for Fill { + fn default() -> Self { + Self::Color(Background::default()) + } +} + +impl From for Fill { + fn from(color: Hsla) -> Self { + Self::Color(color.into()) + } +} + +impl From for Fill { + fn from(color: Rgba) -> Self { + Self::Color(color.into()) + } +} + +impl From for Fill { + fn from(background: Background) -> Self { + Self::Color(background) + } +} + +impl From for HighlightStyle { + fn from(other: TextStyle) -> Self { + Self::from(&other) + } +} + +impl From<&TextStyle> for HighlightStyle { + fn from(other: &TextStyle) -> Self { + Self { + color: Some(other.color), + font_weight: Some(other.font_weight), + font_style: Some(other.font_style), + background_color: other.background_color, + underline: other.underline, + strikethrough: other.strikethrough, + fade_out: None, + } + } +} + +impl HighlightStyle { + /// Create a highlight style with just a color + pub fn color(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } + /// Blend this highlight style with another. + /// Non-continuous properties, like font_weight and font_style, are overwritten. + #[must_use] + pub fn highlight(self, other: HighlightStyle) -> Self { + Self { + color: other + .color + .map(|other_color| { + if let Some(color) = self.color { + color.blend(other_color) + } else { + other_color + } + }) + .or(self.color), + font_weight: other.font_weight.or(self.font_weight), + font_style: other.font_style.or(self.font_style), + background_color: other.background_color.or(self.background_color), + underline: other.underline.or(self.underline), + strikethrough: other.strikethrough.or(self.strikethrough), + fade_out: other + .fade_out + .map(|source_fade| { + self.fade_out + .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.)) + .unwrap_or(source_fade) + }) + .or(self.fade_out), + } + } +} + +impl From for HighlightStyle { + fn from(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_weight: FontWeight) -> Self { + Self { + font_weight: Some(font_weight), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(color: Rgba) -> Self { + Self { + color: Some(color.into()), + ..Default::default() + } + } +} + +/// Combine and merge the highlights and ranges in the two iterators. +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + let mut endpoints = Vec::new(); + let mut highlights = Vec::new(); + for (range, highlight) in a.into_iter().chain(b) { + if !range.is_empty() { + let highlight_id = highlights.len(); + endpoints.push((range.start, highlight_id, true)); + endpoints.push((range.end, highlight_id, false)); + highlights.push(highlight); + } + } + endpoints.sort_unstable_by_key(|(position, _, _)| *position); + let mut endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let current_style = active_styles + .iter() + .fold(HighlightStyle::default(), |acc, highlight_id| { + acc.highlight(highlights[*highlight_id]) + }); + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +/// Used to control how child nodes are aligned. +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum AlignItems { + /// Items are packed toward the start of the axis + Start, + /// Items are packed toward the end of the axis + End, + /// Items are packed towards the flex-relative start of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to End. In all other cases it is equivalent to Start. + FlexStart, + /// Items are packed towards the flex-relative end of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to Start. In all other cases it is equivalent to End. + FlexEnd, + /// Items are packed along the center of the cross axis + Center, + /// Items are aligned such as their baselines align + Baseline, + /// Stretch to fill the container + Stretch, +} +/// Used to control how child nodes are aligned. +/// Does not apply to Flexbox, and will be ignored if specified on a flex container +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items) +pub type JustifyItems = AlignItems; +/// Used to control how the specified nodes is aligned. +/// Overrides the parent Node's `AlignItems` property. +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self) +pub type AlignSelf = AlignItems; +/// Used to control how the specified nodes is aligned. +/// Overrides the parent Node's `JustifyItems` property. +/// Does not apply to Flexbox, and will be ignored if specified on a flex child +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self) +pub type JustifySelf = AlignItems; + +/// Sets the distribution of space between and around content items +/// For Flexbox it controls alignment in the cross axis +/// For Grid it controls alignment in the block axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum AlignContent { + /// Items are packed toward the start of the axis + Start, + /// Items are packed toward the end of the axis + End, + /// Items are packed towards the flex-relative start of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to End. In all other cases it is equivalent to Start. + FlexStart, + /// Items are packed towards the flex-relative end of the axis. + /// + /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent + /// to Start. In all other cases it is equivalent to End. + FlexEnd, + /// Items are centered around the middle of the axis + Center, + /// Items are stretched to fill the container + Stretch, + /// The first and last items are aligned flush with the edges of the container (no gap) + /// The gap between items is distributed evenly. + SpaceBetween, + /// The gap between the first and last items is exactly THE SAME as the gap between items. + /// The gaps are distributed evenly + SpaceEvenly, + /// The gap between the first and last items is exactly HALF the gap between items. + /// The gaps are distributed evenly in proportion to these ratios. + SpaceAround, +} + +/// Sets the distribution of space between and around content items +/// For Flexbox it controls alignment in the main axis +/// For Grid it controls alignment in the inline axis +/// +/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) +pub type JustifyContent = AlignContent; + +/// Sets the layout used for the children of this node +/// +/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Display { + /// The children will follow the block layout algorithm + Block, + /// The children will follow the flexbox layout algorithm + #[default] + Flex, + /// The children will follow the CSS Grid layout algorithm + Grid, + /// The children will not be laid out, and will follow absolute positioning + None, +} + +/// Controls whether flex items are forced onto one line or can wrap onto multiple lines. +/// +/// Defaults to [`FlexWrap::NoWrap`] +/// +/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum FlexWrap { + /// Items will not wrap and stay on a single line + #[default] + NoWrap, + /// Items will wrap according to this item's [`FlexDirection`] + Wrap, + /// Items will wrap in the opposite direction to this item's [`FlexDirection`] + WrapReverse, +} + +/// The direction of the flexbox layout main axis. +/// +/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary). +/// Adding items will cause them to be positioned adjacent to each other along the main axis. +/// By varying this value throughout your tree, you can create complex axis-aligned layouts. +/// +/// Items are always aligned relative to the cross axis, and justified relative to the main axis. +/// +/// The default behavior is [`FlexDirection::Row`]. +/// +/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property) +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum FlexDirection { + /// Defines +x as the main axis + /// + /// Items will be added from left to right in a row. + #[default] + Row, + /// Defines +y as the main axis + /// + /// Items will be added from top to bottom in a column. + Column, + /// Defines -x as the main axis + /// + /// Items will be added from right to left in a row. + RowReverse, + /// Defines -y as the main axis + /// + /// Items will be added from bottom to top in a column. + ColumnReverse, +} + +/// How children overflowing their container should affect layout +/// +/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should +/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout, +/// the main ones being: +/// +/// - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based +/// - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property) +/// +/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for +/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`. +/// +/// +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Overflow { + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node *should* contribute to the scroll region of its parent. + #[default] + Visible, + /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Clip, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Hidden, + /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved + /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property. + /// Content that overflows this node should *not* contribute to the scroll region of its parent. + Scroll, +} + +/// The positioning strategy for this item. +/// +/// This controls both how the origin is determined for the [`Style::position`] field, +/// and whether or not the item will be controlled by flexbox's layout algorithm. +/// +/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position), +/// which can be unintuitive. +/// +/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)] +// Copy of taffy::style type of the same name, to derive JsonSchema. +pub enum Position { + /// The offset is computed relative to the final position given by the layout algorithm. + /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end. + #[default] + Relative, + /// The offset is computed relative to this item's closest positioned ancestor, if any. + /// Otherwise, it is placed relative to the origin. + /// No space is created for the item in the page layout, and its size will not be altered. + /// + /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object. + Absolute, +} + +impl From for taffy::style::AlignItems { + fn from(value: AlignItems) -> Self { + match value { + AlignItems::Start => Self::START, + AlignItems::End => Self::END, + AlignItems::FlexStart => Self::FLEX_START, + AlignItems::FlexEnd => Self::FLEX_END, + AlignItems::Center => Self::CENTER, + AlignItems::Baseline => Self::BASELINE, + AlignItems::Stretch => Self::STRETCH, + } + } +} + +impl From for taffy::style::AlignContent { + fn from(value: AlignContent) -> Self { + match value { + AlignContent::Start => Self::START, + AlignContent::End => Self::END, + AlignContent::FlexStart => Self::FLEX_START, + AlignContent::FlexEnd => Self::FLEX_END, + AlignContent::Center => Self::CENTER, + AlignContent::Stretch => Self::STRETCH, + AlignContent::SpaceBetween => Self::SPACE_BETWEEN, + AlignContent::SpaceEvenly => Self::SPACE_EVENLY, + AlignContent::SpaceAround => Self::SPACE_AROUND, + } + } +} + +impl From for taffy::style::Display { + fn from(value: Display) -> Self { + match value { + Display::Block => Self::Block, + Display::Flex => Self::Flex, + Display::Grid => Self::Grid, + Display::None => Self::None, + } + } +} + +impl From for taffy::style::FlexWrap { + fn from(value: FlexWrap) -> Self { + match value { + FlexWrap::NoWrap => Self::NoWrap, + FlexWrap::Wrap => Self::Wrap, + FlexWrap::WrapReverse => Self::WrapReverse, + } + } +} + +impl From for taffy::style::FlexDirection { + fn from(value: FlexDirection) -> Self { + match value { + FlexDirection::Row => Self::Row, + FlexDirection::Column => Self::Column, + FlexDirection::RowReverse => Self::RowReverse, + FlexDirection::ColumnReverse => Self::ColumnReverse, + } + } +} + +impl From for taffy::style::Overflow { + fn from(value: Overflow) -> Self { + match value { + Overflow::Visible => Self::Visible, + Overflow::Clip => Self::Clip, + Overflow::Hidden => Self::Hidden, + Overflow::Scroll => Self::Scroll, + } + } +} + +impl From for taffy::style::Position { + fn from(value: Position) -> Self { + match value { + Position::Relative => Self::Relative, + Position::Absolute => Self::Absolute, + } + } +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, px, red, yellow}; + + use super::*; + + use util_macros::perf; + + #[perf] + fn test_basic_highlight_style_combination() { + let style_a = HighlightStyle::default(); + let style_b = HighlightStyle::default(); + let style_a = style_a.highlight(style_b); + assert_eq!( + style_a, + HighlightStyle::default(), + "Combining empty styles should not produce a non-empty style." + ); + + let mut style_b = HighlightStyle { + color: Some(red()), + strikethrough: Some(StrikethroughStyle { + thickness: px(2.), + color: Some(blue()), + }), + fade_out: Some(0.), + font_style: Some(FontStyle::Italic), + font_weight: Some(FontWeight(300.)), + background_color: Some(yellow()), + underline: Some(UnderlineStyle { + thickness: px(2.), + color: Some(red()), + wavy: true, + }), + }; + let expected_style = style_b; + + let style_a = style_a.highlight(style_b); + assert_eq!( + style_a, expected_style, + "Blending an empty style with another style should return the other style" + ); + + let style_b = style_b.highlight(Default::default()); + assert_eq!( + style_b, expected_style, + "Blending a style with an empty style should not change the style." + ); + + let mut style_c = expected_style; + + let style_d = HighlightStyle { + color: Some(blue().alpha(0.7)), + strikethrough: Some(StrikethroughStyle { + thickness: px(4.), + color: Some(crate::red()), + }), + fade_out: Some(0.), + font_style: Some(FontStyle::Oblique), + font_weight: Some(FontWeight(800.)), + background_color: Some(green()), + underline: Some(UnderlineStyle { + thickness: px(4.), + color: None, + wavy: false, + }), + }; + + let expected_style = HighlightStyle { + color: Some(red().blend(blue().alpha(0.7))), + strikethrough: Some(StrikethroughStyle { + thickness: px(4.), + color: Some(red()), + }), + // TODO this does not seem right + fade_out: Some(0.), + font_style: Some(FontStyle::Oblique), + font_weight: Some(FontWeight(800.)), + background_color: Some(green()), + underline: Some(UnderlineStyle { + thickness: px(4.), + color: None, + wavy: false, + }), + }; + + let style_c = style_c.highlight(style_d); + assert_eq!( + style_c, expected_style, + "Blending styles should blend properties where possible and override all others" + ); + } + + #[perf] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(blue()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(blue()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } + + #[perf] + fn test_text_style_refinement() { + let mut style = Style::default(); + style.refine(&StyleRefinement::default().text_size(px(20.0))); + style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD)); + + assert_eq!( + Some(AbsoluteLength::from(px(20.0))), + style.text_style().unwrap().font_size + ); + + assert_eq!( + Some(FontWeight::SEMIBOLD), + style.text_style().unwrap().font_weight + ); + } +} diff --git a/vendor/gpui-pre/src/styled.rs b/vendor/gpui-pre/src/styled.rs new file mode 100644 index 0000000..a24982a --- /dev/null +++ b/vendor/gpui-pre/src/styled.rs @@ -0,0 +1,904 @@ +use crate::{ + self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle, + DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle, + FontWeight, GridPlacement, GridTemplate, GridTemplateMinSize, Hsla, JustifyContent, Length, + SharedString, StrikethroughStyle, StyleRefinement, TextAlign, TextOverflow, + TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, +}; +pub use gpui_macros::{ + border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, + overflow_style_methods, padding_style_methods, position_style_methods, + visibility_style_methods, +}; +const ELLIPSIS: SharedString = SharedString::new_static("…"); + +/// A trait for elements that can be styled. +/// Use this to opt-in to a utility CSS-like styling API. +// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv +#[cfg_attr( + all(any(feature = "inspector", debug_assertions), not(rust_analyzer)), + gpui_macros::derive_inspector_reflection +)] +pub trait Styled: Sized { + /// Returns a reference to the style memory of this element. + fn style(&mut self) -> &mut StyleRefinement; + + gpui_macros::style_helpers!(); + gpui_macros::visibility_style_methods!(); + gpui_macros::margin_style_methods!(); + gpui_macros::padding_style_methods!(); + gpui_macros::position_style_methods!(); + gpui_macros::overflow_style_methods!(); + gpui_macros::cursor_style_methods!(); + gpui_macros::border_style_methods!(); + gpui_macros::box_shadow_style_methods!(); + + /// Sets the display type of the element to `block`. + /// [Docs](https://tailwindcss.com/docs/display) + fn block(mut self) -> Self { + self.style().display = Some(Display::Block); + self + } + + /// Sets the display type of the element to `flex`. + /// [Docs](https://tailwindcss.com/docs/display) + fn flex(mut self) -> Self { + self.style().display = Some(Display::Flex); + self + } + + /// Sets the display type of the element to `grid`. + /// [Docs](https://tailwindcss.com/docs/display) + fn grid(mut self) -> Self { + self.style().display = Some(Display::Grid); + self + } + + /// Sets the display type of the element to `none`. + /// [Docs](https://tailwindcss.com/docs/display) + fn hidden(mut self) -> Self { + self.style().display = Some(Display::None); + self + } + + /// Set the space to be reserved for rendering the scrollbar. + /// + /// This will only affect the layout of the element when overflow for this element is set to + /// `Overflow::Scroll`. + fn scrollbar_width(mut self, width: impl Into) -> Self { + self.style().scrollbar_width = Some(width.into()); + self + } + + /// Sets the whitespace of the element to `normal`. + /// [Docs](https://tailwindcss.com/docs/whitespace#normal) + fn whitespace_normal(mut self) -> Self { + self.text_style().white_space = Some(WhiteSpace::Normal); + self + } + + /// Sets the whitespace of the element to `nowrap`. + /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) + fn whitespace_nowrap(mut self) -> Self { + self.text_style().white_space = Some(WhiteSpace::Nowrap); + self + } + + /// Sets the truncate overflowing text with an ellipsis (…) at the end if needed. + /// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis) + fn text_ellipsis(mut self) -> Self { + self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS)); + self + } + + /// Sets the truncate overflowing text with an ellipsis (…) at the start if needed. + /// Typically more adequate for file paths where the end is more important than the beginning. + /// Note: This doesn't exist in Tailwind CSS. + fn text_ellipsis_start(mut self) -> Self { + self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS)); + self + } + + /// Sets the truncate overflowing text with an ellipsis (…) in the middle if needed. + /// Preserves the beginning and end of the text. Useful for filenames. + /// Note: This doesn't exist in Tailwind CSS. + fn text_ellipsis_middle(mut self) -> Self { + self.text_style().text_overflow = Some(TextOverflow::TruncateMiddle(ELLIPSIS)); + self + } + + /// Sets the text overflow behavior of the element. + fn text_overflow(mut self, overflow: TextOverflow) -> Self { + self.text_style().text_overflow = Some(overflow); + self + } + + /// Set the text alignment of the element. + fn text_align(mut self, align: TextAlign) -> Self { + self.text_style().text_align = Some(align); + self + } + + /// Sets the text alignment to left + fn text_left(mut self) -> Self { + self.text_align(TextAlign::Left) + } + + /// Sets the text alignment to center + fn text_center(mut self) -> Self { + self.text_align(TextAlign::Center) + } + + /// Sets the text alignment to right + fn text_right(mut self) -> Self { + self.text_align(TextAlign::Right) + } + + /// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed. + /// [Docs](https://tailwindcss.com/docs/text-overflow#truncate) + fn truncate(mut self) -> Self { + self.overflow_hidden().whitespace_nowrap().text_ellipsis() + } + + /// Sets number of lines to show before truncating the text. + /// [Docs](https://tailwindcss.com/docs/line-clamp) + fn line_clamp(mut self, lines: usize) -> Self { + let mut text_style = self.text_style(); + text_style.line_clamp = Some(lines); + self.overflow_hidden() + } + + /// Sets the flex direction of the element to `column`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column) + fn flex_col(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Column); + self + } + + /// Sets the flex direction of the element to `column-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse) + fn flex_col_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::ColumnReverse); + self + } + + /// Sets the flex direction of the element to `row`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row) + fn flex_row(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::Row); + self + } + + /// Sets the flex direction of the element to `row-reverse`. + /// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse) + fn flex_row_reverse(mut self) -> Self { + self.style().flex_direction = Some(FlexDirection::RowReverse); + self + } + + /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#flex-1) + fn flex_1(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(relative(0.).into()); + self + } + + /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#auto) + fn flex_auto(mut self) -> Self { + self.style().flex_grow = Some(1.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. + /// [Docs](https://tailwindcss.com/docs/flex#initial) + fn flex_initial(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(1.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the element to prevent a flex item from growing or shrinking. + /// [Docs](https://tailwindcss.com/docs/flex#none) + fn flex_none(mut self) -> Self { + self.style().flex_grow = Some(0.); + self.style().flex_shrink = Some(0.); + self.style().flex_basis = Some(Length::Auto); + self + } + + /// Sets the initial size of flex items for this element. + /// [Docs](https://tailwindcss.com/docs/flex-basis) + fn flex_basis(mut self, basis: impl Into) -> Self { + self.style().flex_basis = Some(basis.into()); + self + } + + /// Sets the flex item's grow factor. + /// [Docs](https://tailwindcss.com/docs/flex-grow) + fn flex_grow(mut self, grow: f32) -> Self { + self.style().flex_grow = Some(grow); + self + } + + /// Disables flex item growth (flex-grow: 0). + /// [Docs](https://tailwindcss.com/docs/flex-grow#dont-grow) + fn flex_grow_0(mut self) -> Self { + self.style().flex_grow = Some(0.); + self + } + + /// Enables flex item growth (flex-grow: 1). + /// [Docs](https://tailwindcss.com/docs/flex-grow#grow-1) + fn flex_grow_1(mut self) -> Self { + self.style().flex_grow = Some(1.); + self + } + + /// Sets the flex item's shrink factor. + /// [Docs](https://tailwindcss.com/docs/flex-shrink) + fn flex_shrink(mut self, shrink: f32) -> Self { + self.style().flex_shrink = Some(shrink); + self + } + + /// Disables flex item shrinking (flex-shrink: 0). + /// [Docs](https://tailwindcss.com/docs/flex-shrink#dont-shrink) + fn flex_shrink_0(mut self) -> Self { + self.style().flex_shrink = Some(0.); + self + } + + /// Enables flex item shrinking (flex-shrink: 1). + /// [Docs](https://tailwindcss.com/docs/flex-shrink#shrink-1) + fn flex_shrink_1(mut self) -> Self { + self.style().flex_shrink = Some(1.); + self + } + + /// Sets the element to allow flex items to wrap. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-normally) + fn flex_wrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::Wrap); + self + } + + /// Sets the element wrap flex items in the reverse direction. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#wrap-reversed) + fn flex_wrap_reverse(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::WrapReverse); + self + } + + /// Sets the element to prevent flex items from wrapping, causing inflexible items to overflow the container if necessary. + /// [Docs](https://tailwindcss.com/docs/flex-wrap#dont-wrap) + fn flex_nowrap(mut self) -> Self { + self.style().flex_wrap = Some(FlexWrap::NoWrap); + self + } + + /// Sets the element to align flex items to the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#start) + fn items_start(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexStart); + self + } + + /// Sets the element to align flex items to the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#end) + fn items_end(mut self) -> Self { + self.style().align_items = Some(AlignItems::FlexEnd); + self + } + + /// Sets the element to align flex items along the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#center) + fn items_center(mut self) -> Self { + self.style().align_items = Some(AlignItems::Center); + self + } + + /// Sets the element to align flex items along the baseline of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#baseline) + fn items_baseline(mut self) -> Self { + self.style().align_items = Some(AlignItems::Baseline); + self + } + + /// Sets the element to stretch flex items to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-items#stretch) + fn items_stretch(mut self) -> Self { + self.style().align_items = Some(AlignItems::Stretch); + self + } + + /// Sets how this specific element is aligned along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#start) + fn self_start(mut self) -> Self { + self.style().align_self = Some(AlignSelf::Start); + self + } + + /// Sets this element to align against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#end) + fn self_end(mut self) -> Self { + self.style().align_self = Some(AlignSelf::End); + self + } + + /// Sets this element to align against the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#start) + fn self_flex_start(mut self) -> Self { + self.style().align_self = Some(AlignSelf::FlexStart); + self + } + + /// Sets this element to align against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#end) + fn self_flex_end(mut self) -> Self { + self.style().align_self = Some(AlignSelf::FlexEnd); + self + } + + /// Sets this element to align along the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#center) + fn self_center(mut self) -> Self { + self.style().align_self = Some(AlignSelf::Center); + self + } + + /// Sets this element to align along the baseline of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#baseline) + fn self_baseline(mut self) -> Self { + self.style().align_self = Some(AlignSelf::Baseline); + self + } + + /// Sets this element to stretch to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-self#stretch) + fn self_stretch(mut self) -> Self { + self.style().align_self = Some(AlignSelf::Stretch); + self + } + + /// Sets the element to justify flex items against the start of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#start) + fn justify_start(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Start); + self + } + + /// Sets the element to justify flex items against the end of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#end) + fn justify_end(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::End); + self + } + + /// Sets the element to justify flex items along the center of the container's main axis. + /// [Docs](https://tailwindcss.com/docs/justify-content#center) + fn justify_center(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::Center); + self + } + + /// Sets the element to justify flex items along the container's main axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) + fn justify_between(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceBetween); + self + } + + /// Sets the element to justify items along the container's main axis such + /// that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) + fn justify_around(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceAround); + self + } + + /// Sets the element to justify items along the container's main axis such + /// that there is an equal amount of space around each item, but also + /// accounting for the doubling of space you would normally see between + /// each item when using justify-around. + /// [Docs](https://tailwindcss.com/docs/justify-content#space-evenly) + fn justify_evenly(mut self) -> Self { + self.style().justify_content = Some(JustifyContent::SpaceEvenly); + self + } + + /// Sets the element to pack content items in their default position as if no align-content value was set. + /// [Docs](https://tailwindcss.com/docs/align-content#normal) + fn content_normal(mut self) -> Self { + self.style().align_content = None; + self + } + + /// Sets the element to pack content items in the center of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#center) + fn content_center(mut self) -> Self { + self.style().align_content = Some(AlignContent::Center); + self + } + + /// Sets the element to pack content items against the start of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#start) + fn content_start(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexStart); + self + } + + /// Sets the element to pack content items against the end of the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#end) + fn content_end(mut self) -> Self { + self.style().align_content = Some(AlignContent::FlexEnd); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-between) + fn content_between(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceBetween); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space on each side of each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-around) + fn content_around(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceAround); + self + } + + /// Sets the element to pack content items along the container's cross axis + /// such that there is an equal amount of space between each item. + /// [Docs](https://tailwindcss.com/docs/align-content#space-evenly) + fn content_evenly(mut self) -> Self { + self.style().align_content = Some(AlignContent::SpaceEvenly); + self + } + + /// Sets the element to allow content items to fill the available space along the container's cross axis. + /// [Docs](https://tailwindcss.com/docs/align-content#stretch) + fn content_stretch(mut self) -> Self { + self.style().align_content = Some(AlignContent::Stretch); + self + } + + /// Sets the aspect ratio of the element. + /// [Docs](https://tailwindcss.com/docs/aspect-ratio) + fn aspect_ratio(mut self, ratio: f32) -> Self { + self.style().aspect_ratio = Some(ratio); + self + } + + /// Sets the aspect ratio of the element to 1/1 – equal width and height. + /// [Docs](https://tailwindcss.com/docs/aspect-ratio) + fn aspect_square(mut self) -> Self { + self.style().aspect_ratio = Some(1.0); + self + } + + /// Sets the background color of the element. + fn bg(mut self, fill: F) -> Self + where + F: Into, + Self: Sized, + { + self.style().background = Some(fill.into()); + self + } + + /// Sets the border style of the element. + fn border_dashed(mut self) -> Self { + self.style().border_style = Some(BorderStyle::Dashed); + self + } + + /// Returns a mutable reference to the text style that has been configured on this element. + fn text_style(&mut self) -> &mut TextStyleRefinement { + let style: &mut StyleRefinement = self.style(); + &mut style.text + } + + /// Sets the text color of this element. + /// + /// This value cascades to its child elements. + fn text_color(mut self, color: impl Into) -> Self { + self.text_style().color = Some(color.into()); + self + } + + /// Sets the font weight of this element + /// + /// This value cascades to its child elements. + fn font_weight(mut self, weight: FontWeight) -> Self { + self.text_style().font_weight = Some(weight); + self + } + + /// Sets the background color of this element. + /// + /// This value cascades to its child elements. + fn text_bg(mut self, bg: impl Into) -> Self { + self.text_style().background_color = Some(bg.into()); + self + } + + /// Sets the text size of this element. + /// + /// This value cascades to its child elements. + fn text_size(mut self, size: impl Into) -> Self { + self.text_style().font_size = Some(size.into()); + self + } + + /// Sets the text size to 'extra small'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xs(mut self) -> Self { + self.text_style().font_size = Some(rems(0.75).into()); + self + } + + /// Sets the text size to 'small'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_sm(mut self) -> Self { + self.text_style().font_size = Some(rems(0.875).into()); + self + } + + /// Sets the text size to 'base'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_base(mut self) -> Self { + self.text_style().font_size = Some(rems(1.0).into()); + self + } + + /// Sets the text size to 'large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_lg(mut self) -> Self { + self.text_style().font_size = Some(rems(1.125).into()); + self + } + + /// Sets the text size to 'extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_xl(mut self) -> Self { + self.text_style().font_size = Some(rems(1.25).into()); + self + } + + /// Sets the text size to 'extra extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_2xl(mut self) -> Self { + self.text_style().font_size = Some(rems(1.5).into()); + self + } + + /// Sets the text size to 'extra extra extra large'. + /// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size) + fn text_3xl(mut self) -> Self { + self.text_style().font_size = Some(rems(1.875).into()); + self + } + + /// Sets the font style of the element to italic. + /// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn italic(mut self) -> Self { + self.text_style().font_style = Some(FontStyle::Italic); + self + } + + /// Sets the font style of the element to normal (not italic). + /// [Docs](https://tailwindcss.com/docs/font-style#displaying-text-normally) + fn not_italic(mut self) -> Self { + self.text_style().font_style = Some(FontStyle::Normal); + self + } + + /// Sets the text decoration to underline. + /// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text) + fn underline(mut self) -> Self { + let style = self.text_style(); + style.underline = Some(UnderlineStyle { + thickness: px(1.), + ..Default::default() + }); + self + } + + /// Sets the decoration of the text to have a line through it. + /// [Docs](https://tailwindcss.com/docs/text-decoration-line#adding-a-line-through-text) + fn line_through(mut self) -> Self { + let style = self.text_style(); + style.strikethrough = Some(StrikethroughStyle { + thickness: px(1.), + ..Default::default() + }); + self + } + + /// Removes the text decoration on this element. + /// + /// This value cascades to its child elements. + fn text_decoration_none(mut self) -> Self { + self.text_style().underline = None; + self + } + + /// Sets the color for the underline on this element + fn text_decoration_color(mut self, color: impl Into) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.color = Some(color.into()); + self + } + + /// Sets the text decoration style to a solid line. + /// [Docs](https://tailwindcss.com/docs/text-decoration-style) + fn text_decoration_solid(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = false; + self + } + + /// Sets the text decoration style to a wavy line. + /// [Docs](https://tailwindcss.com/docs/text-decoration-style) + fn text_decoration_wavy(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.wavy = true; + self + } + + /// Sets the text decoration to be 0px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_0(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(0.); + self + } + + /// Sets the text decoration to be 1px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_1(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(1.); + self + } + + /// Sets the text decoration to be 2px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_2(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(2.); + self + } + + /// Sets the text decoration to be 4px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_4(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(4.); + self + } + + /// Sets the text decoration to be 8px thick. + /// [Docs](https://tailwindcss.com/docs/text-decoration-thickness) + fn text_decoration_8(mut self) -> Self { + let style = self.text_style(); + let underline = style.underline.get_or_insert_with(Default::default); + underline.thickness = px(8.); + self + } + + /// Sets the font family of this element and its children. + fn font_family(mut self, family_name: impl Into) -> Self { + self.text_style().font_family = Some(family_name.into()); + self + } + + /// Sets the font features of this element and its children. + fn font_features(mut self, features: FontFeatures) -> Self { + self.text_style().font_features = Some(features); + self + } + + /// Sets the font of this element and its children. + fn font(mut self, font: Font) -> Self { + let Font { + family, + features, + fallbacks, + weight, + style, + } = font; + + let text_style = self.text_style(); + text_style.font_family = Some(family); + text_style.font_features = Some(features); + text_style.font_weight = Some(weight); + text_style.font_style = Some(style); + text_style.font_fallbacks = fallbacks; + + self + } + + /// Sets the line height of this element and its children. + fn line_height(mut self, line_height: impl Into) -> Self { + self.text_style().line_height = Some(line_height.into()); + self + } + + /// Sets the opacity of this element and its children. + fn opacity(mut self, opacity: f32) -> Self { + self.style().opacity = Some(opacity); + self + } + + /// Sets the grid columns of this element. + fn grid_cols(mut self, cols: u16) -> Self { + self.style().grid_cols = Some(GridTemplate { + repeat: cols, + min_size: GridTemplateMinSize::Zero, + }); + self + } + + /// Sets the grid columns with min-content minimum sizing. + /// Unlike grid_cols, it won't shrink to width 0 in AvailableSpace::MinContent constraints. + fn grid_cols_min_content(mut self, cols: u16) -> Self { + self.style().grid_cols = Some(GridTemplate { + repeat: cols, + min_size: GridTemplateMinSize::MinContent, + }); + self + } + + /// Sets the grid columns with max-content maximum sizing for content-based column widths. + fn grid_cols_max_content(mut self, cols: u16) -> Self { + self.style().grid_cols = Some(GridTemplate { + repeat: cols, + min_size: GridTemplateMinSize::MaxContent, + }); + self + } + + /// Sets the grid rows of this element. + fn grid_rows(mut self, rows: u16) -> Self { + self.style().grid_rows = Some(GridTemplate { + repeat: rows, + min_size: GridTemplateMinSize::Zero, + }); + self + } + + /// Sets the grid rows with min-content minimum sizing. + /// Unlike grid_rows, it won't shrink to height 0 in AvailableSpace::MinContent constraints. + fn grid_rows_min_content(mut self, rows: u16) -> Self { + self.style().grid_rows = Some(GridTemplate { + repeat: rows, + min_size: GridTemplateMinSize::MinContent, + }); + self + } + + /// Sets the grid rows with max-content maximum sizing for content-based row heights. + fn grid_rows_max_content(mut self, rows: u16) -> Self { + self.style().grid_rows = Some(GridTemplate { + repeat: rows, + min_size: GridTemplateMinSize::MaxContent, + }); + self + } + + /// Sets the column start of this element. + fn col_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Line(start); + self + } + + /// Sets the column start of this element to auto. + fn col_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.start = GridPlacement::Auto; + self + } + + /// Sets the column end of this element. + fn col_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Line(end); + self + } + + /// Sets the column end of this element to auto. + fn col_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column.end = GridPlacement::Auto; + self + } + + /// Sets the column span of this element. + fn col_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn col_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + + /// Sets the row start of this element. + fn row_start(mut self, start: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Line(start); + self + } + + /// Sets the row start of this element to "auto" + fn row_start_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.start = GridPlacement::Auto; + self + } + + /// Sets the row end of this element. + fn row_end(mut self, end: i16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Line(end); + self + } + + /// Sets the row end of this element to "auto" + fn row_end_auto(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row.end = GridPlacement::Auto; + self + } + + /// Sets the row span of this element. + fn row_span(mut self, span: u16) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span); + self + } + + /// Sets the row span of this element. + fn row_span_full(mut self) -> Self { + let grid_location = self.style().grid_location_mut(); + grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1); + self + } + + /// Draws a debug border around this element. + #[cfg(debug_assertions)] + fn debug(mut self) -> Self { + self.style().debug = Some(true); + self + } + + /// Draws a debug border on all conforming elements below this element. + #[cfg(debug_assertions)] + fn debug_below(mut self) -> Self { + self.style().debug_below = Some(true); + self + } +} diff --git a/vendor/gpui-pre/src/subscription.rs b/vendor/gpui-pre/src/subscription.rs new file mode 100644 index 0000000..b0c55a3 --- /dev/null +++ b/vendor/gpui-pre/src/subscription.rs @@ -0,0 +1,351 @@ +use collections::BTreeMap; +use gpui_util::post_inc; +use std::{ + cell::{Cell, RefCell}, + fmt::Debug, + rc::Rc, +}; + +pub(crate) struct SubscriberSet( + Rc>>, +); + +impl Clone for SubscriberSet { + fn clone(&self) -> Self { + SubscriberSet(self.0.clone()) + } +} + +struct SubscriberSetState { + subscribers: BTreeMap>>>, + next_subscriber_id: usize, +} + +struct Subscriber { + active: Rc>, + dropped: Rc>, + callback: Callback, +} + +impl SubscriberSet +where + EmitterKey: 'static + Ord + Clone + Debug, + Callback: 'static, +{ + pub fn new() -> Self { + Self(Rc::new(RefCell::new(SubscriberSetState { + subscribers: Default::default(), + next_subscriber_id: 0, + }))) + } + + /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions + /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. + /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter + /// to activate the [`Subscription`]. + pub fn insert( + &self, + emitter_key: EmitterKey, + callback: Callback, + ) -> (Subscription, impl FnOnce() + use) { + let active = Rc::new(Cell::new(false)); + let dropped = Rc::new(Cell::new(false)); + let mut lock = self.0.borrow_mut(); + let subscriber_id = post_inc(&mut lock.next_subscriber_id); + lock.subscribers + .entry(emitter_key.clone()) + .or_default() + .get_or_insert_with(Default::default) + .insert( + subscriber_id, + Subscriber { + active: active.clone(), + dropped: dropped.clone(), + callback, + }, + ); + let this = self.0.clone(); + + let subscription = Subscription { + unsubscribe: Some(Box::new(move || { + dropped.set(true); + + let mut lock = this.borrow_mut(); + let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else { + return; + }; + + if let Some(subscribers) = subscribers { + subscribers.remove(&subscriber_id); + if subscribers.is_empty() { + lock.subscribers.remove(&emitter_key); + } + } + })), + }; + (subscription, move || active.set(true)) + } + + pub fn remove( + &self, + emitter: &EmitterKey, + ) -> impl IntoIterator + use { + let subscribers = self.0.borrow_mut().subscribers.remove(emitter); + subscribers + .unwrap_or_default() + .map(|s| s.into_values()) + .into_iter() + .flatten() + .filter_map(|subscriber| { + if subscriber.active.get() { + Some(subscriber.callback) + } else { + None + } + }) + } + + /// Call the given callback for each subscriber to the given emitter. + /// If the callback returns false, the subscriber is removed. + pub fn retain(&self, emitter: &EmitterKey, mut f: F) + where + F: FnMut(&mut Callback) -> bool, + { + let Some(mut subscribers) = self + .0 + .borrow_mut() + .subscribers + .get_mut(emitter) + .and_then(|s| s.take()) + else { + return; + }; + + subscribers.retain(|_, subscriber| { + if !subscriber.active.get() { + return true; + } + if subscriber.dropped.get() { + return false; + } + let keep = f(&mut subscriber.callback); + keep && !subscriber.dropped.get() + }); + let mut lock = self.0.borrow_mut(); + + // Add any new subscribers that were added while invoking the callback. + if let Some(Some(new_subscribers)) = lock.subscribers.remove(emitter) { + subscribers.extend(new_subscribers); + } + + if !subscribers.is_empty() { + lock.subscribers.insert(emitter.clone(), Some(subscribers)); + } + } +} + +/// A handle to a subscription created by GPUI. When dropped, the subscription +/// is cancelled and the callback will no longer be invoked. +#[must_use] +pub struct Subscription { + unsubscribe: Option>, +} + +impl Subscription { + /// Creates a new subscription with a callback that gets invoked when + /// this subscription is dropped. + pub fn new(unsubscribe: impl 'static + FnOnce()) -> Self { + Self { + unsubscribe: Some(Box::new(unsubscribe)), + } + } + + /// Detaches the subscription from this handle. The callback will + /// continue to be invoked until the entities it has been + /// subscribed to are dropped + pub fn detach(mut self) { + self.unsubscribe.take(); + } + + /// Joins two subscriptions into a single subscription. Detach will + /// detach both interior subscriptions. + pub fn join(mut subscription_a: Self, mut subscription_b: Self) -> Self { + let a_unsubscribe = subscription_a.unsubscribe.take(); + let b_unsubscribe = subscription_b.unsubscribe.take(); + Self { + unsubscribe: Some(Box::new(move || { + if let Some(self_unsubscribe) = a_unsubscribe { + self_unsubscribe(); + } + if let Some(other_unsubscribe) = b_unsubscribe { + other_unsubscribe(); + } + })), + } + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + if let Some(unsubscribe) = self.unsubscribe.take() { + unsubscribe(); + } + } +} + +impl std::fmt::Debug for Subscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Subscription").finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Global, TestApp}; + + #[test] + fn test_unsubscribe_during_callback_with_insert() { + struct TestGlobal; + impl Global for TestGlobal {} + + let mut app = TestApp::new(); + app.set_global(TestGlobal); + + let observer_a_count = Rc::new(Cell::new(0usize)); + let observer_b_count = Rc::new(Cell::new(0usize)); + + let sub_a: Rc>> = Default::default(); + let sub_b: Rc>> = Default::default(); + + // Observer A fires first (lower subscriber_id). It drops itself and + // inserts a new observer for the same global. + *sub_a.borrow_mut() = Some(app.update({ + let count = observer_a_count.clone(); + let sub_a = sub_a.clone(); + move |cx| { + cx.observe_global::(move |cx| { + count.set(count.get() + 1); + sub_a.borrow_mut().take(); + cx.observe_global::(|_| {}).detach(); + }) + } + })); + + // Observer B fires second. It just drops itself. + *sub_b.borrow_mut() = Some(app.update({ + let count = observer_b_count.clone(); + let sub_b = sub_b.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + count.set(count.get() + 1); + sub_b.borrow_mut().take(); + }) + } + })); + + // Both fire once. + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(observer_a_count.get(), 1); + assert_eq!(observer_b_count.get(), 1); + + // Neither should fire again — both dropped their subscriptions. + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(observer_a_count.get(), 1); + assert_eq!(observer_b_count.get(), 1, "orphaned subscriber fired again"); + } + + #[test] + fn test_callback_dropped_by_earlier_callback_does_not_fire() { + struct TestGlobal; + impl Global for TestGlobal {} + + let mut app = TestApp::new(); + app.set_global(TestGlobal); + + let observer_b_count = Rc::new(Cell::new(0usize)); + let sub_b: Rc>> = Default::default(); + + // Observer A fires first and drops B's subscription. + app.update({ + let sub_b = sub_b.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + sub_b.borrow_mut().take(); + }) + .detach(); + } + }); + + // Observer B fires second — but A already dropped it. + *sub_b.borrow_mut() = Some(app.update({ + let count = observer_b_count.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + count.set(count.get() + 1); + }) + } + })); + + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!( + observer_b_count.get(), + 0, + "B should not fire — A dropped its subscription" + ); + } + + #[test] + fn test_self_drop_during_callback() { + struct TestGlobal; + impl Global for TestGlobal {} + + let mut app = TestApp::new(); + app.set_global(TestGlobal); + + let count = Rc::new(Cell::new(0usize)); + let sub: Rc>> = Default::default(); + + *sub.borrow_mut() = Some(app.update({ + let count = count.clone(); + let sub = sub.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + count.set(count.get() + 1); + sub.borrow_mut().take(); + }) + } + })); + + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(count.get(), 1); + + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(count.get(), 1, "should not fire after self-drop"); + } + + #[test] + fn test_subscription_drop() { + struct TestGlobal; + impl Global for TestGlobal {} + + let mut app = TestApp::new(); + app.set_global(TestGlobal); + + let count = Rc::new(Cell::new(0usize)); + + let subscription = app.update({ + let count = count.clone(); + move |cx| { + cx.observe_global::(move |_cx| { + count.set(count.get() + 1); + }) + } + }); + + drop(subscription); + + app.update(|cx| cx.set_global(TestGlobal)); + assert_eq!(count.get(), 0, "should not fire after drop"); + } +} diff --git a/vendor/gpui-pre/src/svg_renderer.rs b/vendor/gpui-pre/src/svg_renderer.rs new file mode 100644 index 0000000..798fb2a --- /dev/null +++ b/vendor/gpui-pre/src/svg_renderer.rs @@ -0,0 +1,525 @@ +use crate::{ + AssetSource, DevicePixels, IsZero, RenderImage, Result, SharedString, Size, + swap_rgba_pa_to_bgra, +}; +use image::Frame; +use resvg::tiny_skia::Pixmap; +use smallvec::SmallVec; +use std::{ + hash::Hash, + sync::{Arc, LazyLock, OnceLock}, +}; + +#[cfg(target_os = "macos")] +const EMOJI_FONT_FAMILIES: &[&str] = &["Apple Color Emoji", ".AppleColorEmojiUI"]; + +#[cfg(target_os = "windows")] +const EMOJI_FONT_FAMILIES: &[&str] = &["Segoe UI Emoji", "Segoe UI Symbol"]; + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +const EMOJI_FONT_FAMILIES: &[&str] = &[ + "Noto Color Emoji", + "Emoji One", + "Twitter Color Emoji", + "JoyPixels", +]; + +#[cfg(not(any( + target_os = "macos", + target_os = "windows", + target_os = "linux", + target_os = "freebsd", +)))] +const EMOJI_FONT_FAMILIES: &[&str] = &[]; + +fn is_emoji_presentation(c: char) -> bool { + static EMOJI_PRESENTATION_REGEX: LazyLock = + LazyLock::new(|| regex::Regex::new("\\p{Emoji_Presentation}").unwrap()); + let mut buf = [0u8; 4]; + EMOJI_PRESENTATION_REGEX.is_match(c.encode_utf8(&mut buf)) +} + +fn font_has_char(db: &usvg::fontdb::Database, id: usvg::fontdb::ID, ch: char) -> bool { + db.with_face_data(id, |font_data, face_index| { + ttf_parser::Face::parse(font_data, face_index) + .ok() + .and_then(|face| face.glyph_index(ch)) + .is_some() + }) + .unwrap_or(false) +} + +fn select_emoji_font( + ch: char, + fonts: &[usvg::fontdb::ID], + db: &usvg::fontdb::Database, + families: &[&str], +) -> Option { + for family_name in families { + let query = usvg::fontdb::Query { + families: &[usvg::fontdb::Family::Name(family_name)], + weight: usvg::fontdb::Weight(400), + stretch: usvg::fontdb::Stretch::Normal, + style: usvg::fontdb::Style::Normal, + }; + + let Some(id) = db.query(&query) else { + continue; + }; + + if fonts.contains(&id) || !font_has_char(db, id, ch) { + continue; + } + + return Some(id); + } + + None +} + +/// When rendering SVGs, we render them at twice the size to get a higher-quality result. +pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.; + +#[derive(Clone, PartialEq, Hash, Eq)] +#[expect(missing_docs)] +pub struct RenderSvgParams { + pub path: SharedString, + pub size: Size, +} + +#[derive(Clone)] +/// A struct holding everything necessary to render SVGs. +pub struct SvgRenderer { + asset_source: Arc, + usvg_options: Arc>, +} + +/// A parsed SVG document that can be rasterized at any scale. +/// +/// Produced by [`SvgRenderer::parse_svg`] and rasterized by +/// [`SvgRenderer::render_parsed`]. Parsing resolves fonts and converts text +/// to paths, so callers that need to rasterize the same SVG at multiple +/// scales should retain this value to avoid re-paying the parse cost. +pub struct ParsedSvg(usvg::Tree); + +/// The size in which to rasterize the SVG. +#[derive(Clone, Copy)] +pub enum SvgSize { + /// A width in device pixels. The SVG retains its aspect ratio. + Size(Size), + /// An exact width and height in device pixels. + ExactSize(Size), + /// A logical scaling factor to apply to the size provided by the SVG. + ScaleFactor(f32), +} + +impl From for SvgSize { + fn from(scale_factor: f32) -> Self { + Self::ScaleFactor(scale_factor) + } +} + +impl SvgRenderer { + /// Creates a new SVG renderer with the provided asset source. + pub fn new(asset_source: Arc) -> Self { + static SYSTEM_FONT_DB: LazyLock> = LazyLock::new(|| { + let mut db = usvg::fontdb::Database::new(); + db.load_system_fonts(); + Arc::new(db) + }); + + // Build the enriched font DB lazily on first SVG render rather than + // eagerly at construction time. This avoids the expensive deep-clone + // of the system font database for code paths that never render SVGs + // (e.g. tests). + let enriched_fontdb: Arc>> = Arc::new(OnceLock::new()); + + let default_font_resolver = usvg::FontResolver::default_font_selector(); + let font_resolver = Box::new({ + let asset_source = asset_source.clone(); + move |font: &usvg::Font, db: &mut Arc| { + if db.is_empty() { + let fontdb = enriched_fontdb.get_or_init(|| { + let mut db = (**SYSTEM_FONT_DB).clone(); + load_bundled_fonts(&*asset_source, &mut db); + fix_generic_font_families(&mut db); + Arc::new(db) + }); + *db = fontdb.clone(); + } + if let Some(id) = default_font_resolver(font, db) { + return Some(id); + } + // fontdb doesn't recognize CSS system font keywords like "system-ui" + // or "ui-sans-serif", so fall back to sans-serif before any face. + let sans_query = usvg::fontdb::Query { + families: &[usvg::fontdb::Family::SansSerif], + ..Default::default() + }; + db.query(&sans_query) + .or_else(|| db.faces().next().map(|f| f.id)) + } + }); + let default_fallback_selection = usvg::FontResolver::default_fallback_selector(); + let fallback_selection = Box::new( + move |ch: char, fonts: &[usvg::fontdb::ID], db: &mut Arc| { + if is_emoji_presentation(ch) { + if let Some(id) = select_emoji_font(ch, fonts, db.as_ref(), EMOJI_FONT_FAMILIES) + { + return Some(id); + } + } + + default_fallback_selection(ch, fonts, db) + }, + ); + let options = usvg::Options { + font_resolver: usvg::FontResolver { + select_font: font_resolver, + select_fallback: fallback_selection, + }, + ..Default::default() + }; + Self { + asset_source, + usvg_options: Arc::new(options), + } + } + + /// Parses SVG data into a [`ParsedSvg`] that can be rasterized at any scale. + pub fn parse_svg(&self, bytes: &[u8]) -> Result { + usvg::Tree::from_data(bytes, &self.usvg_options).map(ParsedSvg) + } + + /// Rasterizes a previously parsed SVG into an image buffer. + pub fn render_parsed( + &self, + svg: &ParsedSvg, + size: impl Into, + ) -> Result, usvg::Error> { + let (size, image_scale_factor) = match size.into() { + SvgSize::Size(size) => (SvgSize::Size(size), 1.0), + SvgSize::ExactSize(size) => (SvgSize::ExactSize(size), 1.0), + SvgSize::ScaleFactor(scale_factor) => ( + SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR), + SMOOTH_SVG_SCALE_FACTOR, + ), + }; + let pixmap = rasterize_tree(&svg.0, size)?; + let mut buffer = + image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap(); + + for pixel in buffer.chunks_exact_mut(4) { + swap_rgba_pa_to_bgra(pixel); + } + + let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)])); + image.scale_factor = image_scale_factor; + Ok(Arc::new(image)) + } + + /// Renders the given bytes into an image buffer. + pub fn render_single_frame( + &self, + bytes: &[u8], + scale_factor: f32, + ) -> Result, usvg::Error> { + let svg = self.parse_svg(bytes)?; + self.render_parsed(&svg, scale_factor) + } + + pub(crate) fn render_alpha_mask( + &self, + params: &RenderSvgParams, + bytes: Option<&[u8]>, + ) -> Result, Vec)>> { + anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size"); + + let render_pixmap = |bytes| { + let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?; + + // Convert the pixmap's pixels into an alpha mask. + let size = Size::new( + DevicePixels(pixmap.width() as i32), + DevicePixels(pixmap.height() as i32), + ); + let alpha_mask = pixmap + .pixels() + .iter() + .map(|p| p.alpha()) + .collect::>(); + + Ok(Some((size, alpha_mask))) + }; + + if let Some(bytes) = bytes { + render_pixmap(bytes) + } else if let Some(bytes) = self.asset_source.load(¶ms.path)? { + render_pixmap(&bytes) + } else { + Ok(None) + } + } + + fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?; + rasterize_tree(&tree, size) + } +} + +fn rasterize_tree(tree: &usvg::Tree, size: SvgSize) -> Result { + // Cap the size of the rendered pixmap to avoid texture allocation panics + // Related issue: #56466 + const MAX_SIZE: f32 = 8192.0; + + let svg_size = tree.size(); + let (mut width, mut height) = match size { + SvgSize::Size(size) => { + let scale = i32::from(size.width) as f32 / svg_size.width(); + (svg_size.width() * scale, svg_size.height() * scale) + } + SvgSize::ExactSize(size) => (i32::from(size.width) as f32, i32::from(size.height) as f32), + SvgSize::ScaleFactor(scale) => (svg_size.width() * scale, svg_size.height() * scale), + }; + + if width > MAX_SIZE { + log::warn!("Attempted to render pixmap where width ({width}) > MAX_SIZE ({MAX_SIZE})"); + } + if height > MAX_SIZE { + log::warn!("Attempted to render pixmap where height ({height}) > MAX_SIZE ({MAX_SIZE})"); + } + let scale = (MAX_SIZE / width).min(MAX_SIZE / height).min(1.0); + width *= scale; + height *= scale; + + // Render the SVG to a pixmap with the specified width and height. + let mut pixmap = resvg::tiny_skia::Pixmap::new(width as u32, height as u32) + .ok_or(usvg::Error::InvalidSize)?; + + let transform = resvg::tiny_skia::Transform::from_scale( + width / svg_size.width(), + height / svg_size.height(), + ); + + resvg::render(tree, transform, &mut pixmap.as_mut()); + + Ok(pixmap) +} + +fn load_bundled_fonts(asset_source: &dyn AssetSource, db: &mut usvg::fontdb::Database) { + let font_paths = [ + "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf", + "fonts/lilex/Lilex-Regular.ttf", + ]; + for path in font_paths { + match asset_source.load(path) { + Ok(Some(data)) => db.load_font_data(data.into_owned()), + Ok(None) => log::warn!("Bundled font not found: {path}"), + Err(error) => log::warn!("Failed to load bundled font {path}: {error}"), + } + } +} + +// fontdb defaults generic families to Microsoft fonts ("Arial", "Times New Roman") +// which aren't installed on most Linux systems. fontconfig normally overrides these, +// but when it fails the defaults remain and all generic family queries return None. +fn fix_generic_font_families(db: &mut usvg::fontdb::Database) { + use usvg::fontdb::{Family, Query}; + + let families_and_fallbacks: &[(Family<'_>, &str)] = &[ + (Family::SansSerif, "IBM Plex Sans"), + // No serif font bundled; use sans-serif as best available fallback. + (Family::Serif, "IBM Plex Sans"), + (Family::Monospace, "Lilex"), + (Family::Cursive, "IBM Plex Sans"), + (Family::Fantasy, "IBM Plex Sans"), + ]; + + for (family, fallback_name) in families_and_fallbacks { + let query = Query { + families: &[*family], + ..Default::default() + }; + if db.query(&query).is_none() { + match family { + Family::SansSerif => db.set_sans_serif_family(*fallback_name), + Family::Serif => db.set_serif_family(*fallback_name), + Family::Monospace => db.set_monospace_family(*fallback_name), + Family::Cursive => db.set_cursive_family(*fallback_name), + Family::Fantasy => db.set_fantasy_family(*fallback_name), + _ => {} + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use usvg::fontdb::{Database, Family, Query}; + + const IBM_PLEX_REGULAR: &[u8] = + include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"); + const LILEX_REGULAR: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"); + + #[test] + fn renders_parsed_svg_at_requested_size() -> Result<()> { + let renderer = SvgRenderer::new(Arc::new(())); + let svg = renderer.parse_svg( + br#""#, + )?; + let requested_size = Size::new(DevicePixels(24), DevicePixels(12)); + let image = renderer.render_parsed(&svg, SvgSize::ExactSize(requested_size))?; + + assert_eq!(image.size(0), requested_size); + Ok(()) + } + + #[test] + fn preserves_aspect_ratio_for_width_constrained_size() -> Result<()> { + let renderer = SvgRenderer::new(Arc::new(())); + let svg = renderer.parse_svg( + br#""#, + )?; + let image = renderer.render_parsed( + &svg, + SvgSize::Size(Size::new(DevicePixels(24), DevicePixels(24))), + )?; + + assert_eq!(image.size(0), Size::new(DevicePixels(24), DevicePixels(12))); + Ok(()) + } + + fn db_with_bundled_fonts() -> Database { + let mut db = Database::new(); + db.load_font_data(IBM_PLEX_REGULAR.to_vec()); + db.load_font_data(LILEX_REGULAR.to_vec()); + db + } + + #[test] + fn text_with_split_glyph_clusters_in_mixed_fonts_does_not_panic() { + let mut db = Database::new(); + db.load_font_data(IBM_PLEX_REGULAR.to_vec()); + db.load_font_data(LILEX_REGULAR.to_vec()); + let options = usvg::Options { + fontdb: std::sync::Arc::new(db), + ..Default::default() + }; + + // A base letter followed by a stack of combining marks. Under HarfBuzz's + // default cluster merging every mark glyph shares the base's byte index, + // which is the "glyph splitting" condition that triggered the panic. The + // chunk must use two different fonts so the buggy merge path runs. + let zalgo = "e\u{0301}\u{0302}\u{0303}\u{0304}\u{0306}\u{0307}\u{0308}\u{030a}"; + let svg = format!( + r#"{zalgo}{zalgo}"# + ); + + // Before the fix this aborts via panic with a message like + // "removal index (is 5) should be < len (is 5)". + usvg::Tree::from_data(svg.as_bytes(), &options) + .expect("SVG with mixed-font text should parse"); + } + + #[test] + fn test_is_emoji_presentation() { + let cases = [ + ("a", false), + ("Z", false), + ("1", false), + ("#", false), + ("*", false), + ("漢", false), + ("中", false), + ("カ", false), + ("©", false), + ("♥", false), + ("😀", true), + ("✅", true), + ("🇺🇸", true), + // SVG fallback is not cluster-aware yet + ("©️", false), + ("♥️", false), + ("1️⃣", false), + ]; + for (s, expected) in cases { + assert_eq!( + is_emoji_presentation(s.chars().next().unwrap()), + expected, + "for char {:?}", + s + ); + } + } + + #[test] + fn fix_generic_font_families_sets_all_families() { + let mut db = db_with_bundled_fonts(); + fix_generic_font_families(&mut db); + + let families = [ + Family::SansSerif, + Family::Serif, + Family::Monospace, + Family::Cursive, + Family::Fantasy, + ]; + + for family in families { + let query = Query { + families: &[family], + ..Default::default() + }; + assert!( + db.query(&query).is_some(), + "Expected generic family {family:?} to resolve after fix_generic_font_families" + ); + } + } + + #[test] + fn test_select_emoji_font_skips_family_without_glyph() { + let mut db = db_with_bundled_fonts(); + + let ibm_plex_sans = db + .query(&usvg::fontdb::Query { + families: &[usvg::fontdb::Family::Name("IBM Plex Sans")], + weight: usvg::fontdb::Weight(400), + stretch: usvg::fontdb::Stretch::Normal, + style: usvg::fontdb::Style::Normal, + }) + .unwrap(); + let lilex = db + .query(&usvg::fontdb::Query { + families: &[usvg::fontdb::Family::Name("Lilex")], + weight: usvg::fontdb::Weight(400), + stretch: usvg::fontdb::Stretch::Normal, + style: usvg::fontdb::Style::Normal, + }) + .unwrap(); + let selected = select_emoji_font('│', &[], &db, &["IBM Plex Sans", "Lilex"]).unwrap(); + + assert_eq!(selected, lilex); + assert!(!font_has_char(&db, ibm_plex_sans, '│')); + assert!(font_has_char(&db, selected, '│')); + } + + #[test] + fn fix_generic_font_families_monospace_resolves_to_lilex() { + let mut db = db_with_bundled_fonts(); + fix_generic_font_families(&mut db); + + let query = Query { + families: &[Family::Monospace], + ..Default::default() + }; + let id = db.query(&query).expect("Monospace should resolve"); + let face = db.face(id).expect("Face should exist"); + assert!( + face.families.iter().any(|(name, _)| name.contains("Lilex")), + "Monospace should map to Lilex, got {:?}", + face.families + ); + } +} diff --git a/vendor/gpui-pre/src/tab_stop.rs b/vendor/gpui-pre/src/tab_stop.rs new file mode 100644 index 0000000..bde651a --- /dev/null +++ b/vendor/gpui-pre/src/tab_stop.rs @@ -0,0 +1,615 @@ +use std::fmt::Debug; + +use ::sum_tree::SumTree; +use collections::FxHashMap; +use sum_tree::Bias; + +use crate::{FocusHandle, FocusId}; + +/// Represents a collection of focus handles using the tab-index APIs. +#[derive(Debug)] +pub(crate) struct TabStopMap { + current_path: TabStopPath, + pub(crate) insertion_history: Vec, + by_id: FxHashMap, + order: SumTree, +} + +#[derive(Debug, Clone)] +pub enum TabStopOperation { + Insert(FocusHandle), + Group(TabIndex), + GroupEnd, +} + +impl TabStopOperation { + fn focus_handle(&self) -> Option<&FocusHandle> { + match self { + TabStopOperation::Insert(focus_handle) => Some(focus_handle), + _ => None, + } + } +} + +type TabIndex = isize; + +#[derive(Debug, Default, PartialEq, Eq, Clone, Ord, PartialOrd)] +struct TabStopPath(smallvec::SmallVec<[TabIndex; 6]>); + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct TabStopNode { + /// Path to access the node in the tree + /// The final node in the list is a leaf node corresponding to an actual focus handle, + /// all other nodes are group nodes + path: TabStopPath, + /// index into the backing array of nodes. Corresponds to insertion order + node_insertion_index: usize, + + /// Whether this node is a tab stop + tab_stop: bool, +} + +impl Ord for TabStopNode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.path + .cmp(&other.path) + .then(self.node_insertion_index.cmp(&other.node_insertion_index)) + } +} + +impl PartialOrd for TabStopNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Default for TabStopMap { + fn default() -> Self { + Self { + current_path: TabStopPath::default(), + insertion_history: Vec::new(), + by_id: FxHashMap::default(), + order: SumTree::new(()), + } + } +} + +impl TabStopMap { + pub fn insert(&mut self, focus_handle: &FocusHandle) { + self.insertion_history + .push(TabStopOperation::Insert(focus_handle.clone())); + let mut path = self.current_path.clone(); + path.0.push(focus_handle.tab_index); + let order = TabStopNode { + node_insertion_index: self.insertion_history.len() - 1, + tab_stop: focus_handle.tab_stop, + path, + }; + self.by_id.insert(focus_handle.id, order.clone()); + self.order.insert_or_replace(order, ()); + } + + pub fn begin_group(&mut self, tab_index: isize) { + self.insertion_history + .push(TabStopOperation::Group(tab_index)); + self.current_path.0.push(tab_index); + } + + pub fn end_group(&mut self) { + self.insertion_history.push(TabStopOperation::GroupEnd); + self.current_path.0.pop(); + } + + pub fn clear(&mut self) { + *self = Self::default(); + self.current_path.0.clear(); + self.insertion_history.clear(); + self.by_id.clear(); + self.order = SumTree::new(()); + } + + pub fn next(&self, focused_id: Option<&FocusId>) -> Option { + let Some(focused_id) = focused_id else { + let first = self.order.first()?; + if first.tab_stop { + return self.focus_handle_for_order(first); + } else { + return self + .next_inner(first) + .and_then(|order| self.focus_handle_for_order(order)); + } + }; + + let Some(node) = self.tab_node_for_focus_id(focused_id) else { + return self.next(None); + }; + let item = self.next_inner(node); + + if let Some(item) = item { + self.focus_handle_for_order(&item) + } else { + self.next(None) + } + } + + fn next_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { + let mut cursor = self.order.cursor::(()); + cursor.seek(&node, Bias::Left); + cursor.next(); + while let Some(item) = cursor.item() + && !item.tab_stop + { + cursor.next(); + } + + cursor.item() + } + + pub fn prev(&self, focused_id: Option<&FocusId>) -> Option { + let Some(focused_id) = focused_id else { + let last = self.order.last()?; + if last.tab_stop { + return self.focus_handle_for_order(last); + } else { + return self + .prev_inner(last) + .and_then(|order| self.focus_handle_for_order(order)); + } + }; + + let Some(node) = self.tab_node_for_focus_id(focused_id) else { + return self.prev(None); + }; + let item = self.prev_inner(node); + + if let Some(item) = item { + self.focus_handle_for_order(&item) + } else { + self.prev(None) + } + } + + fn prev_inner(&self, node: &TabStopNode) -> Option<&TabStopNode> { + let mut cursor = self.order.cursor::(()); + cursor.seek(&node, Bias::Left); + cursor.prev(); + while let Some(item) = cursor.item() + && !item.tab_stop + { + cursor.prev(); + } + + cursor.item() + } + + pub fn replay(&mut self, nodes: &[TabStopOperation]) { + for node in nodes { + match node { + TabStopOperation::Insert(focus_handle) => self.insert(focus_handle), + TabStopOperation::Group(tab_index) => self.begin_group(*tab_index), + TabStopOperation::GroupEnd => self.end_group(), + } + } + } + + pub fn paint_index(&self) -> usize { + self.insertion_history.len() + } + + pub(crate) fn tab_stop_count(&self) -> usize { + self.by_id.values().filter(|node| node.tab_stop).count() + } + + fn focus_handle_for_order(&self, order: &TabStopNode) -> Option { + let handle = self.insertion_history[order.node_insertion_index].focus_handle(); + debug_assert!( + handle.is_some(), + "The order node did not correspond to an element, this is a GPUI bug" + ); + handle.cloned() + } + + fn tab_node_for_focus_id(&self, focused_id: &FocusId) -> Option<&TabStopNode> { + let Some(order) = self.by_id.get(focused_id) else { + return None; + }; + Some(order) + } +} + +mod sum_tree_impl { + use sum_tree::SeekTarget; + + use crate::tab_stop::{TabStopNode, TabStopPath}; + + #[derive(Clone, Debug)] + pub struct TabStopOrderNodeSummary { + max_index: usize, + max_path: TabStopPath, + pub tab_stops: usize, + } + + pub type TabStopCount = usize; + + impl sum_tree::ContextLessSummary for TabStopOrderNodeSummary { + fn zero() -> Self { + TabStopOrderNodeSummary { + max_index: 0, + max_path: TabStopPath::default(), + tab_stops: 0, + } + } + + fn add_summary(&mut self, summary: &Self) { + self.max_index = summary.max_index; + self.max_path = summary.max_path.clone(); + self.tab_stops += summary.tab_stops; + } + } + + impl sum_tree::KeyedItem for TabStopNode { + type Key = Self; + + fn key(&self) -> Self::Key { + self.clone() + } + } + + impl sum_tree::Item for TabStopNode { + type Summary = TabStopOrderNodeSummary; + + fn summary(&self, _cx: ::Context<'_>) -> Self::Summary { + TabStopOrderNodeSummary { + max_index: self.node_insertion_index, + max_path: self.path.clone(), + tab_stops: if self.tab_stop { 1 } else { 0 }, + } + } + } + + impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopCount { + fn zero(_: ::Context<'_>) -> Self { + 0 + } + + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::Context<'_>, + ) { + *self += summary.tab_stops; + } + } + + impl<'a> sum_tree::Dimension<'a, TabStopOrderNodeSummary> for TabStopNode { + fn zero(_: ::Context<'_>) -> Self { + TabStopNode::default() + } + + fn add_summary( + &mut self, + summary: &'a TabStopOrderNodeSummary, + _: ::Context<'_>, + ) { + self.node_insertion_index = summary.max_index; + self.path = summary.max_path.clone(); + } + } + + impl<'a, 'b> SeekTarget<'a, TabStopOrderNodeSummary, TabStopNode> for &'b TabStopNode { + fn cmp( + &self, + cursor_location: &TabStopNode, + _: ::Context<'_>, + ) -> std::cmp::Ordering { + Iterator::cmp(self.path.0.iter(), cursor_location.path.0.iter()).then( + ::cmp( + &self.node_insertion_index, + &cursor_location.node_insertion_index, + ), + ) + } + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools as _; + + use crate::{FocusHandle, FocusId, FocusMap, TabStopMap}; + use std::sync::Arc; + + #[test] + fn test_tab_handles() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab_index_map = TabStopMap::default(); + + let focus_handles = [ + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map), + FocusHandle::new(&focus_map).tab_index(2), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(2), + ]; + + for handle in focus_handles.iter() { + tab_index_map.insert(handle); + } + let expected = [ + focus_handles[0].clone(), + focus_handles[5].clone(), + focus_handles[1].clone(), + focus_handles[2].clone(), + focus_handles[6].clone(), + ]; + + let mut prev = None; + let mut found = vec![]; + for _ in 0..expected.len() { + let handle = tab_index_map.next(prev.as_ref()).unwrap(); + prev = Some(handle.id); + found.push(handle.id); + } + + assert_eq!( + found, + expected.iter().map(|handle| handle.id).collect::>() + ); + + // Select first tab index if no handle is currently focused. + assert_eq!(tab_index_map.next(None), Some(expected[0].clone())); + // Select last tab index if no handle is currently focused. + assert_eq!(tab_index_map.prev(None), expected.last().cloned(),); + + assert_eq!( + tab_index_map.next(Some(&expected[0].id)), + Some(expected[1].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[1].id)), + Some(expected[2].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[2].id)), + Some(expected[3].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[3].id)), + Some(expected[4].clone()) + ); + assert_eq!( + tab_index_map.next(Some(&expected[4].id)), + Some(expected[0].clone()) + ); + + // prev + assert_eq!(tab_index_map.prev(None), Some(expected[4].clone())); + assert_eq!( + tab_index_map.prev(Some(&expected[0].id)), + Some(expected[4].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[1].id)), + Some(expected[0].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[2].id)), + Some(expected[1].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[3].id)), + Some(expected[2].clone()) + ); + assert_eq!( + tab_index_map.prev(Some(&expected[4].id)), + Some(expected[3].clone()) + ); + } + + #[test] + fn test_tab_non_stop_filtering() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab_index_map = TabStopMap::default(); + + // Check that we can query next from a non-stop tab + let tab_non_stop_1 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(1); + let tab_stop_2 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(2); + tab_index_map.insert(&tab_non_stop_1); + tab_index_map.insert(&tab_stop_2); + let result = tab_index_map.next(Some(&tab_non_stop_1.id)).unwrap(); + assert_eq!(result.id, tab_stop_2.id); + + // Check that we skip over non-stop tabs + let tab_stop_0 = FocusHandle::new(&focus_map).tab_stop(true).tab_index(0); + let tab_non_stop_0 = FocusHandle::new(&focus_map).tab_stop(false).tab_index(0); + tab_index_map.insert(&tab_stop_0); + tab_index_map.insert(&tab_non_stop_0); + let result = tab_index_map.next(Some(&tab_stop_0.id)).unwrap(); + assert_eq!(result.id, tab_stop_2.id); + } + + #[must_use] + struct TabStopMapTest { + tab_map: TabStopMap, + focus_map: Arc, + expected: Vec<(usize, FocusId)>, + } + + impl TabStopMapTest { + #[must_use] + fn new() -> Self { + Self { + tab_map: TabStopMap::default(), + focus_map: Arc::new(FocusMap::default()), + expected: Vec::default(), + } + } + + #[must_use] + fn tab_non_stop(mut self, index: isize) -> Self { + let handle = FocusHandle::new(&self.focus_map) + .tab_stop(false) + .tab_index(index); + self.tab_map.insert(&handle); + self + } + + #[must_use] + fn tab_stop(mut self, index: isize, expected: usize) -> Self { + let handle = FocusHandle::new(&self.focus_map) + .tab_stop(true) + .tab_index(index); + self.tab_map.insert(&handle); + self.expected.push((expected, handle.id)); + self.expected.sort_by_key(|(expected, _)| *expected); + self + } + + #[must_use] + fn tab_group(mut self, tab_index: isize, children: impl FnOnce(Self) -> Self) -> Self { + self.tab_map.begin_group(tab_index); + self = children(self); + self.tab_map.end_group(); + self + } + + fn traverse_tab_map( + &self, + traverse: impl Fn(&TabStopMap, Option<&FocusId>) -> Option, + ) -> Vec { + let mut last_focus_id = None; + let mut found = vec![]; + for _ in 0..self.expected.len() { + let handle = traverse(&self.tab_map, last_focus_id.as_ref()).unwrap(); + last_focus_id = Some(handle.id); + found.push(handle.id); + } + found + } + + fn assert(self) { + let mut expected = self.expected.iter().map(|(_, id)| *id).collect_vec(); + + // Check next order + let forward_found = self.traverse_tab_map(|tab_map, prev| tab_map.next(prev)); + assert_eq!(forward_found, expected); + + // Test overflow. Last to first + assert_eq!( + self.tab_map + .next(forward_found.last()) + .map(|handle| handle.id), + expected.first().cloned() + ); + + // Check previous order + let reversed_found = self.traverse_tab_map(|tab_map, prev| tab_map.prev(prev)); + expected.reverse(); + assert_eq!(reversed_found, expected); + + // Test overflow. First to last + assert_eq!( + self.tab_map + .prev(reversed_found.last()) + .map(|handle| handle.id), + expected.first().cloned(), + ); + } + } + + #[test] + fn test_with_disabled_tab_stop() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_non_stop(1) + .tab_stop(2, 1) + .tab_stop(3, 2) + .assert(); + } + + #[test] + fn test_with_multiple_disabled_tab_stops() { + TabStopMapTest::new() + .tab_non_stop(0) + .tab_stop(1, 0) + .tab_non_stop(3) + .tab_stop(3, 1) + .tab_non_stop(4) + .assert(); + } + + #[test] + fn test_tab_group_functionality() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(0, 1) + .tab_group(2, |t| t.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(3, 4) + .tab_stop(4, 5) + .assert() + } + + #[test] + fn test_sibling_groups() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |test| test.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(3, 4) + .tab_stop(4, 5) + .tab_group(6, |test| test.tab_stop(0, 6).tab_stop(1, 7)) + .tab_stop(7, 8) + .tab_stop(8, 9) + .assert(); + } + + #[test] + fn test_nested_group() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |t| { + t.tab_group(0, |t| t.tab_stop(0, 2).tab_stop(1, 3)) + .tab_stop(1, 4) + }) + .tab_stop(3, 5) + .tab_stop(4, 6) + .assert(); + } + + #[test] + fn test_sibling_nested_groups() { + TabStopMapTest::new() + .tab_stop(0, 0) + .tab_stop(1, 1) + .tab_group(2, |builder| { + builder + .tab_stop(0, 2) + .tab_stop(2, 5) + .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) + .tab_group(3, |builder| builder.tab_stop(0, 6).tab_stop(1, 7)) + }) + .tab_stop(3, 8) + .tab_stop(4, 9) + .assert(); + } + + #[test] + fn test_sibling_nested_groups_out_of_order() { + TabStopMapTest::new() + .tab_stop(9, 9) + .tab_stop(8, 8) + .tab_group(7, |builder| { + builder + .tab_stop(0, 2) + .tab_stop(2, 5) + .tab_group(3, |builder| builder.tab_stop(1, 7).tab_stop(0, 6)) + .tab_group(1, |builder| builder.tab_stop(0, 3).tab_stop(1, 4)) + }) + .tab_stop(3, 0) + .tab_stop(4, 1) + .assert(); + } +} diff --git a/vendor/gpui-pre/src/taffy.rs b/vendor/gpui-pre/src/taffy.rs new file mode 100644 index 0000000..b9e583a --- /dev/null +++ b/vendor/gpui-pre/src/taffy.rs @@ -0,0 +1,776 @@ +use crate::{ + AbsoluteLength, App, Bounds, DefiniteLength, Edges, GridTemplate, Length, Pixels, Point, Size, + Style, Window, size, + util::{ + ceil_to_device_pixel, round_half_toward_zero, round_stroke_to_device_pixel, + round_to_device_pixel, + }, +}; +use collections::{FxHashMap, FxHashSet}; +use std::{fmt::Debug, ops::Range}; +use taffy::{ + TaffyTree, TraversePartialTree as _, + geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, + prelude::{max_content, min_content}, + style::AvailableSpace as TaffyAvailableSpace, + tree::NodeId, +}; + +#[cfg(feature = "stacker")] +type StackSafe = stacksafe::StackSafe; +#[cfg(not(feature = "stacker"))] +type StackSafe = T; + +type MeasureFn = + dyn FnMut(Size>, Size, &mut Window, &mut App) -> Size; +type NodeMeasureFn = StackSafe>; + +struct NodeContext { + measure: NodeMeasureFn, +} +pub struct TaffyLayoutEngine { + taffy: TaffyTree, + absolute_layout_bounds: FxHashMap>, + /// Unrounded absolute border-box top-left per-node coordinate in device pixels. + absolute_outer_origins: FxHashMap>, + computed_layouts: FxHashSet, + layout_bounds_scratch_space: Vec, +} + +const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; + +impl TaffyLayoutEngine { + pub fn new() -> Self { + let mut taffy = TaffyTree::new(); + taffy.disable_rounding(); + TaffyLayoutEngine { + taffy, + absolute_layout_bounds: FxHashMap::default(), + absolute_outer_origins: FxHashMap::default(), + computed_layouts: FxHashSet::default(), + layout_bounds_scratch_space: Vec::new(), + } + } + + pub fn clear(&mut self) { + self.taffy.clear(); + self.absolute_layout_bounds.clear(); + self.absolute_outer_origins.clear(); + self.computed_layouts.clear(); + } + + pub fn request_layout( + &mut self, + style: Style, + rem_size: Pixels, + scale_factor: f32, + children: &[LayoutId], + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size, scale_factor); + + if children.is_empty() { + self.taffy + .new_leaf(taffy_style) + .expect(EXPECT_MESSAGE) + .into() + } else { + self.taffy + // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId. + .new_with_children(taffy_style, LayoutId::to_taffy_slice(children)) + .expect(EXPECT_MESSAGE) + .into() + } + } + + pub fn request_measured_layout( + &mut self, + style: Style, + rem_size: Pixels, + scale_factor: f32, + measure: impl FnMut( + Size>, + Size, + &mut Window, + &mut App, + ) -> Size + + 'static, + ) -> LayoutId { + let taffy_style = style.to_taffy(rem_size, scale_factor); + let measure = Box::new(measure) as Box; + #[cfg(feature = "stacker")] + let measure = StackSafe::new(measure); + + self.taffy + .new_leaf_with_context(taffy_style, NodeContext { measure }) + .expect(EXPECT_MESSAGE) + .into() + } + + /// Treats any `auto` dimension of the given node's style as filling `size`. + /// + /// This is applied to window roots before layout so they behave like the + /// root element on the web, which stretches to fill the initial containing + /// block (the viewport) unless given an explicit size. Explicitly styled + /// dimensions are preserved. + pub fn stretch_auto_size_to_fill( + &mut self, + id: LayoutId, + size: Size, + scale_factor: f32, + ) { + let style = self.taffy.style(id.0).expect(EXPECT_MESSAGE); + let stretch_width = style.size.width.is_auto(); + let stretch_height = style.size.height.is_auto(); + if !stretch_width && !stretch_height { + return; + } + let mut style = style.clone(); + if stretch_width { + style.size.width = + taffy::style::Dimension::length(round_to_device_pixel(size.width.0, scale_factor)); + } + if stretch_height { + style.size.height = + taffy::style::Dimension::length(round_to_device_pixel(size.height.0, scale_factor)); + } + self.taffy.set_style(id.0, style).expect(EXPECT_MESSAGE); + } + + // Used to understand performance + #[allow(dead_code)] + fn count_all_children(&self, parent: LayoutId) -> anyhow::Result { + let mut count = 0; + + for child in self.taffy.children(parent.0)? { + // Count this child. + count += 1; + + // Count all of this child's children. + count += self.count_all_children(LayoutId(child))? + } + + Ok(count) + } + + // Used to understand performance + #[allow(dead_code)] + fn max_depth(&self, depth: u32, parent: LayoutId) -> anyhow::Result { + println!( + "{parent:?} at depth {depth} has {} children", + self.taffy.child_count(parent.0) + ); + + let mut max_child_depth = 0; + + for child in self.taffy.children(parent.0)? { + max_child_depth = std::cmp::max(max_child_depth, self.max_depth(0, LayoutId(child))?); + } + + Ok(depth + 1 + max_child_depth) + } + + // Used to understand performance + #[allow(dead_code)] + fn get_edges(&self, parent: LayoutId) -> anyhow::Result> { + let mut edges = Vec::new(); + + for child in self.taffy.children(parent.0)? { + edges.push((parent, LayoutId(child))); + + edges.extend(self.get_edges(LayoutId(child))?); + } + + Ok(edges) + } + + #[cfg_attr(feature = "stacker", stacksafe::stacksafe)] + pub fn compute_layout( + &mut self, + id: LayoutId, + available_space: Size, + window: &mut Window, + cx: &mut App, + ) { + // Leaving this here until we have a better instrumentation approach. + // println!("Laying out {} children", self.count_all_children(id)?); + // println!("Max layout depth: {}", self.max_depth(0, id)?); + + // Output the edges (branches) of the tree in Mermaid format for visualization. + // println!("Edges:"); + // for (a, b) in self.get_edges(id)? { + // println!("N{} --> N{}", u64::from(a), u64::from(b)); + // } + // + + if !self.computed_layouts.insert(id) { + let stack = &mut self.layout_bounds_scratch_space; + stack.push(id); + while let Some(id) = stack.pop() { + self.absolute_layout_bounds.remove(&id); + self.absolute_outer_origins.remove(&id); + stack.extend( + self.taffy + .children(id.into()) + .expect(EXPECT_MESSAGE) + .into_iter() + .map(LayoutId::from), + ); + } + } + + let scale_factor = window.scale_factor(); + + let transform = |v: AvailableSpace| match v { + AvailableSpace::Definite(pixels) => { + AvailableSpace::Definite(Pixels(pixels.0 * scale_factor)) + } + AvailableSpace::MinContent => AvailableSpace::MinContent, + AvailableSpace::MaxContent => AvailableSpace::MaxContent, + }; + let available_space = size( + transform(available_space.width), + transform(available_space.height), + ); + + self.taffy + .compute_layout_with_measure( + id.into(), + available_space.into(), + |known_dimensions, available_space, _id, node_context, _style| { + let Some(node_context) = node_context else { + return taffy::geometry::Size::default(); + }; + + let known_dimensions = Size { + width: known_dimensions.width.map(|e| Pixels(e / scale_factor)), + height: known_dimensions.height.map(|e| Pixels(e / scale_factor)), + }; + + let available_space: Size = available_space.into(); + let untransform = |ev: AvailableSpace| match ev { + AvailableSpace::Definite(pixels) => { + AvailableSpace::Definite(Pixels(pixels.0 / scale_factor)) + } + AvailableSpace::MinContent => AvailableSpace::MinContent, + AvailableSpace::MaxContent => AvailableSpace::MaxContent, + }; + let available_space = size( + untransform(available_space.width), + untransform(available_space.height), + ); + + let measured_size: Size = + (node_context.measure)(known_dimensions, available_space, window, cx); + snap_measured_size_to_device_pixels(measured_size, scale_factor).into() + }, + ) + .expect(EXPECT_MESSAGE); + } + + // Pixel snapping + // + // Painting primitives at non-integer pixel coordinates produces blurry + // output. Pixel snapping converts layout coordinates into integer + // device-pixel coordinates so painted edges land exactly on physical + // pixel boundaries. + // + // Non-integer coordinates can arise for several reasons, including: + // - flex distribution, percentages, centering, and text measurement + // can produce fractional element sizes and positions; + // - at fractional scale factors (for example 125% or 150%), integer + // logical-pixel values can map to non-integer device-pixel values. + // + // We pixel-snap by rounding in device-pixel space, after multiplying + // by `scale_factor`, so that snapping targets physical pixels. Bounds + // are divided by `scale_factor` before being returned to GPUI. + // + // Midpoints are rounded toward zero. This is a stylistic choice: a + // 1-logical-pixel line at 150% scale should render as 1 dp rather than + // 2 dp. + // + // Pixel snapping is done in two phases: + // + // 1. Pre-layout metric snapping. Before Taffy computes layout, all + // authored absolute lengths are rounded in `to_taffy`. This + // includes borders, padding, gaps, and explicit sizes. + // Custom-measured leaf nodes have their measured sizes rounded up + // to integer device-pixel lengths. + // + // 2. Post-layout edge snapping. After Taffy resolves the tree, layout + // relationships such as flex shares, grid tracks, percentages, and + // centering can produce new fractional edge positions. Boxes now + // have edges in absolute coordinates, and snapping must decide + // where those edges land on the device-pixel grid. + // + // Ideally, post-layout snapping would satisfy: + // + // - Edge closure. Two raw layout edges at the same absolute position + // should snap to the same pixel column. + // - Translation stability. A component's internal geometry should not + // change when it moves to a new absolute position. + // + // These goals are in tension because rounding is not associative. + // The simple local schemes make different tradeoffs: + // + // - Absolute edge rounding gives each window coordinate one answer, + // so coincident edges always close globally. But a span's snapped + // length is `round(far) - round(near)`, which may change by 1 dp + // as its absolute origin moves. + // + // - Parent-relative edge rounding rounds each child inside its + // parent's coordinate space. This guarantees translation stability, + // but a shared edge reached through different parents can + // accumulate different rounding, causing non-closure between + // cousins. + // + // - Length rounding rounds each width, height, and thickness + // independently and then places boxes from those rounded lengths. + // Sizes stay stable under translation, but neighboring boxes derive + // their shared boundary from different sources, so closure is not + // guaranteed. + // + // We apply absolute edge rounding for each element's outer box in + // post-layout rounding to preserve closure. Border and padding widths + // are not touched by post-layout rounding; they keep their pre-layout + // rounded value so that they remain stable under translation. + // + // This gives both closure and translation stability in the case that + // all local metrics are integer device-pixel lengths. Pre-layout + // rounding covers that in most cases. The exception is metrics + // resolved by layout relationships, such as percentages. Outer box + // edges will still close globally, and painted border widths are still + // snapped independently, but the raw content-box origin can carry a + // 1dp residual into descendants. + + pub fn layout_bounds(&mut self, id: LayoutId, scale_factor: f32) -> Bounds { + if let Some(layout) = self.absolute_layout_bounds.get(&id).cloned() { + return layout; + } + + let layout = self.taffy.layout(id.into()).expect(EXPECT_MESSAGE); + let layout_location = layout.location; + let layout_size = layout.size; + let parent = self.taffy.parent(id.0); + + let absolute_outer_origin = match parent { + Some(parent_id) => { + let parent_id = LayoutId::from(parent_id); + self.layout_bounds(parent_id, scale_factor); + let parent_origin = *self + .absolute_outer_origins + .get(&parent_id) + .expect("parent absolute outer origin should be cached"); + parent_origin + Point::from(layout_location) + } + None => Point::from(layout_location), + }; + self.absolute_outer_origins + .insert(id, absolute_outer_origin); + + let absolute_far = absolute_outer_origin + Point::from(Size::from(layout_size)); + let snapped_bounds = Bounds::from_corners( + absolute_outer_origin.map(round_half_toward_zero), + absolute_far.map(round_half_toward_zero), + ); + + let bounds = (snapped_bounds / scale_factor).map(Pixels); + self.absolute_layout_bounds.insert(id, bounds); + bounds + } +} + +/// A unique identifier for a layout node, generated when requesting a layout from Taffy +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(transparent)] +pub struct LayoutId(NodeId); + +impl LayoutId { + fn to_taffy_slice(node_ids: &[Self]) -> &[taffy::NodeId] { + // SAFETY: LayoutId is repr(transparent) to taffy::tree::NodeId. + unsafe { std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(node_ids) } + } +} + +impl std::hash::Hash for LayoutId { + fn hash(&self, state: &mut H) { + u64::from(self.0).hash(state); + } +} + +impl From for LayoutId { + fn from(node_id: NodeId) -> Self { + Self(node_id) + } +} + +impl From for NodeId { + fn from(layout_id: LayoutId) -> NodeId { + layout_id.0 + } +} + +fn snap_measured_size_to_device_pixels(size: Size, scale_factor: f32) -> Size { + size.map(|d| ceil_to_device_pixel(d.0.max(0.0), scale_factor)) +} + +fn border_widths_to_taffy( + widths: &Edges, + rem_size: Pixels, + scale_factor: f32, +) -> TaffyRect { + let snap = |w: &AbsoluteLength| { + taffy::style::LengthPercentage::length(round_stroke_to_device_pixel( + w.to_pixels(rem_size).0, + scale_factor, + )) + }; + TaffyRect { + top: snap(&widths.top), + right: snap(&widths.right), + bottom: snap(&widths.bottom), + left: snap(&widths.left), + } +} + +trait ToTaffy { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> Output; +} + +impl ToTaffy for Style { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Style { + use taffy::style_helpers::{fr, length, minmax, repeat}; + + fn to_grid_line( + placement: &Range, + ) -> taffy::Line { + taffy::Line { + start: placement.start.into(), + end: placement.end.into(), + } + } + + fn to_grid_repeat( + unit: &Option, + ) -> Vec> { + unit.map(|template| { + match template.min_size { + // grid-template-*: repeat(, minmax(0, 1fr)); + crate::GridTemplateMinSize::Zero => { + vec![repeat( + template.repeat, + vec![minmax(length(0.0_f32), fr(1.0_f32))], + )] + } + // grid-template-*: repeat(, minmax(min-content, 1fr)); + crate::GridTemplateMinSize::MinContent => { + vec![repeat( + template.repeat, + vec![minmax(min_content(), fr(1.0_f32))], + )] + } + // grid-template-*: repeat(, minmax(0, max-content)) + crate::GridTemplateMinSize::MaxContent => { + vec![repeat( + template.repeat, + vec![minmax(length(0.0_f32), max_content())], + )] + } + } + }) + .unwrap_or_default() + } + + taffy::style::Style { + display: self.display.into(), + overflow: self.overflow.into(), + scrollbar_width: self.scrollbar_width.to_taffy(rem_size, scale_factor), + position: self.position.into(), + inset: self.inset.to_taffy(rem_size, scale_factor), + size: self.size.to_taffy(rem_size, scale_factor), + min_size: self.min_size.to_taffy(rem_size, scale_factor), + max_size: self.max_size.to_taffy(rem_size, scale_factor), + aspect_ratio: self.aspect_ratio, + margin: self.margin.to_taffy(rem_size, scale_factor), + padding: self.padding.to_taffy(rem_size, scale_factor), + border: border_widths_to_taffy(&self.border_widths, rem_size, scale_factor), + align_items: self.align_items.map(|x| x.into()), + align_self: self.align_self.map(|x| x.into()), + align_content: self.align_content.map(|x| x.into()), + justify_content: self.justify_content.map(|x| x.into()), + gap: self.gap.to_taffy(rem_size, scale_factor), + flex_direction: self.flex_direction.into(), + flex_wrap: self.flex_wrap.into(), + flex_basis: self.flex_basis.to_taffy(rem_size, scale_factor), + flex_grow: self.flex_grow, + flex_shrink: self.flex_shrink, + grid_template_rows: to_grid_repeat(&self.grid_rows), + grid_template_columns: to_grid_repeat(&self.grid_cols), + grid_row: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.row)) + .unwrap_or_default(), + grid_column: self + .grid_location + .as_ref() + .map(|location| to_grid_line(&location.column)) + .unwrap_or_default(), + ..Default::default() + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> f32 { + round_to_device_pixel(self.to_pixels(rem_size).0, scale_factor) + } +} + +impl ToTaffy for Length { + fn to_taffy( + &self, + rem_size: Pixels, + scale_factor: f32, + ) -> taffy::prelude::LengthPercentageAuto { + match self { + Length::Definite(length) => length.to_taffy(rem_size, scale_factor), + Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), + } + } +} + +impl ToTaffy for Length { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::prelude::Dimension { + match self { + Length::Definite(length) => length.to_taffy(rem_size, scale_factor), + Length::Auto => taffy::prelude::Dimension::auto(), + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { + match self { + DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentage::percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { + match self { + DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), + DefiniteLength::Fraction(fraction) => { + taffy::style::LengthPercentageAuto::percent(*fraction) + } + } + } +} + +impl ToTaffy for DefiniteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { + match self { + DefiniteLength::Absolute(length) => length.to_taffy(rem_size, scale_factor), + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), + } + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentage { + taffy::style::LengthPercentage::length(self.to_taffy(rem_size, scale_factor)) + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::LengthPercentageAuto { + taffy::style::LengthPercentageAuto::length(self.to_taffy(rem_size, scale_factor)) + } +} + +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Dimension { + taffy::style::Dimension::length(self.to_taffy(rem_size, scale_factor)) + } +} + +impl From> for Point +where + T: Into, + T2: Clone + Debug + Default + PartialEq, +{ + fn from(point: TaffyPoint) -> Point { + Point { + x: point.x.into(), + y: point.y.into(), + } + } +} + +impl From> for TaffyPoint +where + T: Into + Clone + Debug + Default + PartialEq, +{ + fn from(val: Point) -> Self { + TaffyPoint { + x: val.x.into(), + y: val.y.into(), + } + } +} + +impl ToTaffy> for Size +where + T: ToTaffy + Clone + Debug + Default + PartialEq, +{ + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffySize { + TaffySize { + width: self.width.to_taffy(rem_size, scale_factor), + height: self.height.to_taffy(rem_size, scale_factor), + } + } +} + +impl ToTaffy> for Edges +where + T: ToTaffy + Clone + Debug + Default + PartialEq, +{ + fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> TaffyRect { + TaffyRect { + top: self.top.to_taffy(rem_size, scale_factor), + right: self.right.to_taffy(rem_size, scale_factor), + bottom: self.bottom.to_taffy(rem_size, scale_factor), + left: self.left.to_taffy(rem_size, scale_factor), + } + } +} + +impl From> for Size +where + T: Into, + U: Clone + Debug + Default + PartialEq, +{ + fn from(taffy_size: TaffySize) -> Self { + Size { + width: taffy_size.width.into(), + height: taffy_size.height.into(), + } + } +} + +impl From> for TaffySize +where + T: Into + Clone + Debug + Default + PartialEq, +{ + fn from(size: Size) -> Self { + TaffySize { + width: size.width.into(), + height: size.height.into(), + } + } +} + +/// The space available for an element to be laid out in +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] +pub enum AvailableSpace { + /// The amount of space available is the specified number of pixels + Definite(Pixels), + /// The amount of space available is indefinite and the node should be laid out under a min-content constraint + #[default] + MinContent, + /// The amount of space available is indefinite and the node should be laid out under a max-content constraint + MaxContent, +} + +impl AvailableSpace { + /// Returns a `Size` with both width and height set to `AvailableSpace::MinContent`. + /// + /// This function is useful when you want to create a `Size` with the minimum content constraints + /// for both dimensions. + /// + /// # Examples + /// + /// ``` + /// use gpui::AvailableSpace; + /// let min_content_size = AvailableSpace::min_size(); + /// assert_eq!(min_content_size.width, AvailableSpace::MinContent); + /// assert_eq!(min_content_size.height, AvailableSpace::MinContent); + /// ``` + pub const fn min_size() -> Size { + Size { + width: Self::MinContent, + height: Self::MinContent, + } + } +} + +impl From for TaffyAvailableSpace { + fn from(space: AvailableSpace) -> TaffyAvailableSpace { + match space { + AvailableSpace::Definite(Pixels(value)) => TaffyAvailableSpace::Definite(value), + AvailableSpace::MinContent => TaffyAvailableSpace::MinContent, + AvailableSpace::MaxContent => TaffyAvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(space: TaffyAvailableSpace) -> AvailableSpace { + match space { + TaffyAvailableSpace::Definite(value) => AvailableSpace::Definite(Pixels(value)), + TaffyAvailableSpace::MinContent => AvailableSpace::MinContent, + TaffyAvailableSpace::MaxContent => AvailableSpace::MaxContent, + } + } +} + +impl From for AvailableSpace { + fn from(pixels: Pixels) -> Self { + AvailableSpace::Definite(pixels) + } +} + +impl From> for Size { + fn from(size: Size) -> Self { + Size { + width: AvailableSpace::Definite(size.width), + height: AvailableSpace::Definite(size.height), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn border_widths_to_taffy_use_stroke_snapping() { + let border_widths = Edges { + top: Pixels(0.0).into(), + right: Pixels(0.4).into(), + bottom: Pixels(0.5).into(), + left: Pixels(1.6).into(), + }; + let taffy_border = border_widths_to_taffy(&border_widths, Pixels(16.0), 1.0); + + assert_eq!( + taffy_border.top, + taffy::style::LengthPercentage::length(0.0) + ); + assert_eq!( + taffy_border.right, + taffy::style::LengthPercentage::length(1.0) + ); + assert_eq!( + taffy_border.bottom, + taffy::style::LengthPercentage::length(1.0) + ); + assert_eq!( + taffy_border.left, + taffy::style::LengthPercentage::length(2.0) + ); + } +} diff --git a/vendor/gpui-pre/src/test.rs b/vendor/gpui-pre/src/test.rs new file mode 100644 index 0000000..c1c8e66 --- /dev/null +++ b/vendor/gpui-pre/src/test.rs @@ -0,0 +1,172 @@ +//! Test support for GPUI. +//! +//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, +//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run +//! deterministically even in the face of arbitrary parallelism. +//! +//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` +//! or `cargo-nextest`, or another runner of your choice. +//! +//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts +//! as you need. +//! +//! ## Example +//! +//! ``` +//! use gpui; +//! +//! #[gpui::test] +//! async fn test_example(cx: &TestAppContext) { +//! assert!(true) +//! } +//! +//! #[gpui::test] +//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { +//! assert!(true) +//! } +//! ``` +use crate::{Entity, Subscription, TestAppContext, TestDispatcher, calculate_seeds}; +use futures::StreamExt as _; +use proptest::prelude::{Just, Strategy, any}; +use std::{ + env, + panic::{self, RefUnwindSafe, UnwindSafe}, + pin::Pin, +}; + +/// Strategy injected into `#[gpui::property_test]` tests to control the seed +/// given to the scheduler. Doesn't shrink, since all scheduler seeds are +/// equivalent in complexity. If `$SEED` is set, it always uses that value. +/// +/// Note: this function is not intended to be used directly. Rather, it is +/// public so that it can be used from the `property_test` macro. +pub fn seed_strategy() -> impl Strategy { + match std::env::var("SEED") { + Ok(val) => Just(val.parse().unwrap()).boxed(), + Err(_) => any::().no_shrink().boxed(), + } +} + +/// Applies a fixed RNG seed to a proptest config so that case generation +/// is deterministic. Uses `$SEED` if set, otherwise defaults to `0`. +/// This bridges the GPUI `SEED` env var to proptest's RNG seed, so that +/// a single variable controls both the scheduler seed and case generation. +/// +/// Note: this function is not intended to be used directly. Rather, it is +/// public so that it can be used from the `property_test` macro. +pub fn apply_seed_to_proptest_config( + mut config: proptest::test_runner::Config, +) -> proptest::test_runner::Config { + let seed = env::var("SEED") + .ok() + .and_then(|val| val.parse::().ok()) + .unwrap_or(0); + config.rng_seed = proptest::test_runner::RngSeed::Fixed(seed); + config +} + +/// Similar to [`run_test`], but only runs the callback once, allowing +/// [`FnOnce`] callbacks. This is intended for use with the +/// `gpui::property_test` macro and generally should not be used directly. +/// +/// Doesn't support many features of [`run_test`], since these are provided by +/// proptest. +pub fn run_test_once( + seed: u64, + test_fn: Box R>, +) -> R { + let result = panic::catch_unwind(|| { + let dispatcher = TestDispatcher::new(seed); + let scheduler = dispatcher.scheduler().clone(); + let res = test_fn(dispatcher); + scheduler.end_test(); + res + }); + + match result { + Ok(r) => r, + Err(e) => panic::resume_unwind(e), + } +} + +/// Run the given test function with the configured parameters. +/// This is intended for use with the `gpui::test` macro +/// and generally should not be used directly. +pub fn run_test( + num_iterations: usize, + explicit_seeds: &[u64], + max_retries: usize, + test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), + on_fail_fn: Option, +) { + let (seeds, is_multiple_runs) = calculate_seeds(num_iterations as u64, explicit_seeds); + + for seed in seeds { + let mut attempt = 0; + loop { + if is_multiple_runs { + eprintln!("seed = {seed}"); + } + let result = panic::catch_unwind(|| { + let dispatcher = TestDispatcher::new(seed); + let scheduler = dispatcher.scheduler().clone(); + test_fn(dispatcher, seed); + scheduler.end_test(); + }); + + match result { + Ok(_) => break, + Err(error) => { + if attempt < max_retries { + println!("attempt {} failed, retrying", attempt); + attempt += 1; + // The panic payload might itself trigger an unwind on drop: + // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes + std::mem::forget(error); + } else { + if is_multiple_runs { + eprintln!("failing seed: {seed}"); + eprintln!( + "You can rerun from this seed by setting the environmental variable SEED to {seed}" + ); + } + if let Some(on_fail_fn) = on_fail_fn { + on_fail_fn() + } + panic::resume_unwind(error); + } + } + } + } + } +} + +/// A test struct for converting an observation callback into a stream. +pub struct Observation { + rx: Pin>>, + _subscription: Subscription, +} + +impl futures::Stream for Observation { + type Item = T; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } +} + +/// observe returns a stream of the change events from the given `Entity` +pub fn observe(entity: &Entity, cx: &mut TestAppContext) -> Observation<()> { + let (tx, rx) = async_channel::unbounded(); + let _subscription = cx.update(|cx| { + cx.observe(entity, move |_, _| { + let _ = gpui::block_on(tx.send(())); + }) + }); + let rx = Box::pin(rx); + + Observation { rx, _subscription } +} diff --git a/vendor/gpui-pre/src/text_system.rs b/vendor/gpui-pre/src/text_system.rs new file mode 100644 index 0000000..cf6bb10 --- /dev/null +++ b/vendor/gpui-pre/src/text_system.rs @@ -0,0 +1,1505 @@ +mod font_fallbacks; +mod font_features; +mod line; +mod line_layout; +mod line_wrapper; + +pub use font_fallbacks::*; +pub use font_features::*; +pub use line::*; +pub use line_layout::*; +pub use line_wrapper::*; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, + StrikethroughStyle, TextRenderingMode, UnderlineStyle, px, +}; +use anyhow::{Context as _, anyhow}; +use collections::{FxHashMap, FxHashSet}; +use core::fmt; +use derive_more::{Add, Deref, FromStr, Sub}; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::{SmallVec, smallvec}; +use std::{ + borrow::Cow, + cmp, + collections::VecDeque, + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut, Range}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +/// An opaque identifier for a specific font. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +#[repr(C)] +pub struct FontId(pub usize); + +/// An opaque identifier for a specific font family. +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)] +pub struct FontFamilyId(pub usize); + +/// Number of subpixel glyph variants along the X axis. +pub const SUBPIXEL_VARIANTS_X: u8 = 4; + +/// Number of subpixel glyph variants along the Y axis. +pub const SUBPIXEL_VARIANTS_Y: u8 = 1; + +// Leave enough room below the underline for its stroke while keeping it below the baseline. +const UNDERLINE_DESCENT_OFFSET_FACTOR: f32 = 0.618; + +/// Returns the vertical offset used to paint an underline within a line. +pub fn underline_y_offset(line_height: Pixels, ascent: Pixels, descent: Pixels) -> Pixels { + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + descent * UNDERLINE_DESCENT_OFFSET_FACTOR +} + +const MAX_REPORTED_MISSING_GLYPHS: usize = 1024; + +/// The spacing behavior required of a fallback font. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum FallbackFontClass { + /// A proportionally spaced fallback font. + Proportional, + /// A fixed-width fallback font. + Monospace, +} + +/// A grapheme cluster that could not be represented by any available font. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct MissingGlyph { + grapheme: SharedString, + font_class: FallbackFontClass, +} + +impl MissingGlyph { + /// Creates a missing-glyph report. + pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self { + Self { + grapheme, + font_class, + } + } + + /// Returns the unresolved grapheme cluster. + pub fn grapheme(&self) -> &str { + &self.grapheme + } + + /// Returns the spacing behavior required of a fallback font. + pub fn font_class(&self) -> FallbackFontClass { + self.font_class + } +} + +/// Accepts missing glyphs detected by a platform text system. +pub trait MissingGlyphSink: Send + Sync { + /// Reports grapheme clusters that exhausted font fallback. + fn report(&self, missing_glyphs: Vec); +} + +#[derive(Default)] +struct MissingGlyphState { + reported: FxHashSet, + reported_order: VecDeque, + generation: usize, +} + +impl MissingGlyphState { + fn reset(&mut self, generation: usize) { + self.reported.clear(); + self.reported_order.clear(); + self.generation = generation; + } +} + +struct QueuedMissingGlyph { + generation: usize, + missing_glyph: MissingGlyph, +} + +/// Collects missing-glyph reports without invoking application code during layout. +struct MissingGlyphReporter { + generation: Arc, + sender: async_channel::Sender, +} + +impl MissingGlyphSink for MissingGlyphReporter { + fn report(&self, missing_glyphs: Vec) { + if self.sender.is_closed() { + return; + } + + let generation = self.generation.load(Ordering::Acquire); + // Repetitions within a line must not fill the queue before its other + // missing glyphs. Cross-report deduplication belongs to the receiver. + for missing_glyph in missing_glyphs.into_iter().unique() { + let queued = QueuedMissingGlyph { + generation, + missing_glyph, + }; + if self.sender.try_send(queued).is_err() { + break; + } + } + } +} + +impl MissingGlyphReporter { + fn reset(&self) { + self.generation.fetch_add(1, Ordering::AcqRel); + } +} + +/// Receives batches of grapheme clusters that exhausted font fallback. +pub(crate) struct MissingGlyphReceiver { + state: MissingGlyphState, + generation: Arc, + receiver: async_channel::Receiver, +} + +impl MissingGlyphReceiver { + /// Waits until at least one new missing glyph has been observed. + /// + /// # Errors + /// + /// Returns [`async_channel::RecvError`] if the reporting channel is closed. + pub(crate) async fn recv( + &mut self, + ) -> std::result::Result, async_channel::RecvError> { + loop { + let queued = self.receiver.recv().await?; + let mut missing_glyphs = Vec::new(); + for queued in std::iter::once(queued) + .chain(std::iter::from_fn(|| self.receiver.try_recv().ok())) + .take(MAX_REPORTED_MISSING_GLYPHS) + { + let generation = self.generation.load(Ordering::Acquire); + if self.state.generation != generation { + self.state.reset(generation); + missing_glyphs.clear(); + } + if queued.generation != generation + || !self.state.reported.insert(queued.missing_glyph.clone()) + { + continue; + } + self.state + .reported_order + .push_back(queued.missing_glyph.clone()); + missing_glyphs.push(queued.missing_glyph); + if self.state.reported.len() > MAX_REPORTED_MISSING_GLYPHS + && let Some(expired) = self.state.reported_order.pop_front() + { + self.state.reported.remove(&expired); + } + } + if !missing_glyphs.is_empty() { + return Ok(missing_glyphs); + } + // A producer can keep refilling the queue with already-reported + // glyphs. Bound work per poll even when every report is filtered out. + let mut yielded = false; + std::future::poll_fn(|cx| { + if std::mem::replace(&mut yielded, true) { + std::task::Poll::Ready(()) + } else { + cx.waker().wake_by_ref(); + std::task::Poll::Pending + } + }) + .await; + } + } +} + +impl Drop for MissingGlyphReceiver { + fn drop(&mut self) { + self.receiver.close(); + while self.receiver.try_recv().is_ok() {} + } +} + +/// The GPUI text rendering sub system. +pub struct TextSystem { + platform_text_system: Arc, + font_ids_by_font: RwLock>>, + font_metrics: RwLock>, + raster_bounds: RwLock>>, + wrapper_pool: Mutex>>, + font_runs_pool: Mutex>>, + fallback_font_stack: SmallVec<[Font; 2]>, + font_generation: Arc, + missing_glyph_reporter: Arc, + missing_glyph_receiver: Mutex>, +} + +impl TextSystem { + /// Create a new TextSystem with the given platform text system. + pub fn new(platform_text_system: Arc) -> Self { + let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS); + let missing_glyph_generation = Arc::::default(); + TextSystem { + platform_text_system, + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), + fallback_font_stack: smallvec![ + // TODO: Remove this when Linux have implemented setting fallbacks. + font(".ZedMono"), + font(".ZedSans"), + font("Helvetica"), + font("Segoe UI"), // Windows + font("Ubuntu"), // Gnome (Ubuntu) + font("Adwaita Sans"), // Gnome 47 + font("Cantarell"), // Gnome + font("Noto Sans"), // KDE + font("DejaVu Sans"), + font("Arial"), // macOS, Windows + ], + font_generation: Arc::default(), + missing_glyph_reporter: Arc::new(MissingGlyphReporter { + generation: missing_glyph_generation.clone(), + sender, + }), + missing_glyph_receiver: Mutex::new(Some(MissingGlyphReceiver { + state: MissingGlyphState::default(), + generation: missing_glyph_generation, + receiver, + })), + } + } + + /// Get sorted, unique font family names available to the platform text system. + /// + /// Includes fonts registered with [`Self::add_fonts`]. + pub fn all_font_names(&self) -> Vec { + let mut names = self.platform_text_system.all_font_names(); + names.sort_unstable(); + names.dedup(); + names + } + + /// Add a font's data to the text system. + /// + /// Cached font resolution and line layouts are invalidated after installation. + /// Layouts already in progress may complete against the previous font set. + pub fn add_fonts(&self, fonts: Vec>) -> Result<()> { + self.platform_text_system.add_fonts(fonts)?; + self.font_ids_by_font.write().clear(); + self.missing_glyph_reporter.reset(); + self.font_generation.fetch_add(1, Ordering::Release); + Ok(()) + } + + /// Takes the receiver for missing-glyph reports. + /// + /// Only one receiver is available for each text system. Returns `None` when + /// the receiver was already taken or another caller is taking it. + pub(crate) fn take_missing_glyph_receiver(&self) -> Option { + self.missing_glyph_receiver + .try_lock() + .and_then(|mut receiver| receiver.take()) + } + + pub(crate) fn enable_missing_glyph_reporting(&self) { + self.platform_text_system + .set_missing_glyph_sink(Some(self.missing_glyph_reporter.clone())); + } + + pub(crate) fn disable_missing_glyph_reporting(&self) { + self.platform_text_system.set_missing_glyph_sink(None); + self.missing_glyph_reporter.reset(); + } + + #[cfg(test)] + pub(crate) fn report_missing_glyphs_in_test(&self, missing_glyphs: Vec) { + self.missing_glyph_reporter.report(missing_glyphs); + } + + /// Get the FontId for the configure font family and style. + fn font_id(&self, font: &Font) -> Result { + fn clone_font_id_result(font_id: &Result) -> Result { + match font_id { + Ok(font_id) => Ok(*font_id), + Err(err) => Err(anyhow!("{err}")), + } + } + + let font_id = self + .font_ids_by_font + .read() + .get(font) + .map(clone_font_id_result); + if let Some(font_id) = font_id { + font_id + } else { + let font_id = self.platform_text_system.font_id(font); + self.font_ids_by_font + .write() + .insert(font.clone(), clone_font_id_result(&font_id)); + font_id + } + } + + /// Get the Font for the Font Id. + pub fn get_font_for_id(&self, id: FontId) -> Option { + let lock = self.font_ids_by_font.read(); + lock.iter() + .filter_map(|(font, result)| match result { + Ok(font_id) if *font_id == id => Some(font.clone()), + _ => None, + }) + .next() + } + + /// Resolves the specified font, falling back to the default font stack if + /// the font fails to load. + /// + /// # Panics + /// + /// Panics if the font and none of the fallbacks can be resolved. + pub fn resolve_font(&self, font: &Font) -> FontId { + if let Ok(font_id) = self.font_id(font) { + return font_id; + } + for fallback in &self.fallback_font_stack { + if let Ok(font_id) = self.font_id(fallback) { + return font_id; + } + } + + panic!( + "failed to resolve font '{}' or any of the fallbacks: {}", + font.family, + self.fallback_font_stack + .iter() + .map(|fallback| &fallback.family) + .join(", ") + ); + } + + /// Prewarm any system font caches needed to shape text. + /// + /// This may be expensive, so callers should generally invoke it on a + /// background executor. Missing entries are still populated on demand by + /// the normal shaping path. + pub fn prewarm_fonts(&self, fonts: &[Font]) { + let mut font_ids = SmallVec::<[FontId; 8]>::new(); + for font in fonts { + let font_id = self.resolve_font(font); + if !font_ids.contains(&font_id) { + font_ids.push(font_id); + } + } + self.platform_text_system.prewarm_fonts(&font_ids); + } + + /// Get the bounding box for the given font and font size. + /// A font's bounding box is the smallest rectangle that could enclose all glyphs + /// in the font. superimposed over one another. + pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds { + self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size)) + } + + /// Get the typographic bounds for the given character, in the given font and size. + pub fn typographic_bounds( + &self, + font_id: FontId, + font_size: Pixels, + character: char, + ) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, character) + .with_context(|| format!("glyph not found for character '{character}'"))?; + let bounds = self + .platform_text_system + .typographic_bounds(font_id, glyph_id)?; + Ok(self.read_metrics(font_id, |metrics| { + (bounds / metrics.units_per_em as f32 * font_size.0).map(px) + })) + } + + /// Get the advance width for the given character, in the given font and size. + pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result> { + let glyph_id = self + .platform_text_system + .glyph_for_char(font_id, ch) + .with_context(|| format!("glyph not found for character '{ch}'"))?; + let result = self.platform_text_system.advance(font_id, glyph_id)? + / self.units_per_em(font_id) as f32; + + Ok(result * font_size) + } + + // Consider removing this? + /// Returns the shaped layout width of for the given character, in the given font and size. + pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels { + let mut buffer = [0; 4]; + let buffer = ch.encode_utf8(&mut buffer); + self.platform_text_system + .layout_line( + buffer, + font_size, + &[FontRun { + len: buffer.len(), + font_id, + }], + ) + .width + } + + /// Returns the width of an `em`. + /// + /// Uses the width of the `m` character in the given font and size. + pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width) + } + + /// Returns the advance width of an `em`. + /// + /// Uses the advance width of the `m` character in the given font and size. + pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.advance(font_id, font_size, 'm')?.width) + } + + /// Returns the width of an `ch`. + /// + /// Uses the width of the `0` character in the given font and size. + pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width) + } + + /// Returns the advance width of an `ch`. + /// + /// Uses the advance width of the `0` character in the given font and size. + pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result { + Ok(self.advance(font_id, font_size, '0')?.width) + } + + /// Get the number of font size units per 'em square', + /// Per MDN: "an abstract square whose height is the intended distance between + /// lines of type in the same type size" + pub fn units_per_em(&self, font_id: FontId) -> u32 { + self.read_metrics(font_id, |metrics| metrics.units_per_em) + } + + /// Get the height of a capital letter in the given font and size. + pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.cap_height(font_size)) + } + + /// Get the height of the x character in the given font and size. + pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.x_height(font_size)) + } + + /// Get the recommended distance from the baseline for the given font + pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.ascent(font_size)) + } + + /// Get the recommended distance below the baseline for the given font, + /// in single spaced text. + pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.read_metrics(font_id, |metrics| metrics.descent(font_size)) + } + + /// Get the recommended baseline offset for the given font and line height. + pub fn baseline_offset( + &self, + font_id: FontId, + font_size: Pixels, + line_height: Pixels, + ) -> Pixels { + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + } + + fn read_metrics(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T { + let lock = self.font_metrics.upgradable_read(); + + if let Some(metrics) = lock.get(&font_id) { + read(metrics) + } else { + let mut lock = RwLockUpgradableReadGuard::upgrade(lock); + let metrics = lock + .entry(font_id) + .or_insert_with(|| self.platform_text_system.font_metrics(font_id)); + read(metrics) + } + } + + /// Returns a handle to a line wrapper, for the given font and font size. + pub fn line_wrapper(self: &Arc, font: Font, font_size: Pixels) -> LineWrapperHandle { + let lock = &mut self.wrapper_pool.lock(); + let font_id = self.resolve_font(&font); + let wrappers = lock + .entry(FontIdWithSize { font_id, font_size }) + .or_default(); + let wrapper = wrappers + .pop() + .unwrap_or_else(|| LineWrapper::new(font_id, font_size, self.clone())); + + LineWrapperHandle { + wrapper: Some(wrapper), + text_system: self.clone(), + } + } + + /// Get the rasterized size and location of a specific, rendered glyph. + pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(*bounds) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } + } + + pub(crate) fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + ) -> Result<(Size, Vec)> { + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) + } + + /// Returns the dilation level to use for a glyph painted in the given color. + pub(crate) fn glyph_dilation_for_color(&self, color: Hsla) -> u8 { + self.platform_text_system.glyph_dilation_for_color(color) + } + + /// Returns the text rendering mode recommended by the platform for the given font and size. + /// The return value will never be [`TextRenderingMode::PlatformDefault`]. + pub(crate) fn recommended_rendering_mode( + &self, + font_id: FontId, + font_size: Pixels, + ) -> TextRenderingMode { + self.platform_text_system + .recommended_rendering_mode(font_id, font_size) + } +} + +/// The GPUI text layout subsystem. +#[derive(Deref)] +pub struct WindowTextSystem { + line_layout_cache: LineLayoutCache, + #[deref] + text_system: Arc, +} + +impl WindowTextSystem { + /// Create a new WindowTextSystem with the given TextSystem. + pub fn new(text_system: Arc) -> Self { + Self { + line_layout_cache: LineLayoutCache::new( + text_system.platform_text_system.clone(), + text_system.font_generation.clone(), + ), + text_system, + } + } + + pub(crate) fn layout_index(&self) -> LineLayoutIndex { + self.line_layout_cache.layout_index() + } + + pub(crate) fn reuse_layouts(&self, index: Range) { + self.line_layout_cache.reuse_layouts(index) + } + + pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) { + self.line_layout_cache.truncate_layouts(index) + } + + /// Shape the given line, at the given font_size, for painting to the screen. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// + /// Note that this method can only shape a single line of text. It will panic + /// if the text contains newlines. If you need to shape multiple lines of text, + /// use [`Self::shape_text`] instead. + pub fn shape_line( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + ) -> ShapedLine { + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + } + + let layout = self.layout_line(&text, font_size, runs, force_width); + + ShapedLine { + layout, + text, + decoration_runs, + } + } + + /// Shape the given line using a caller-provided content hash as the cache key. + /// + /// This enables cache hits without materializing a contiguous `SharedString` for the text. + /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. + /// + /// Contract (caller enforced): + /// - Same `text_hash` implies identical text content (collision risk accepted by caller). + /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). + /// + /// Like [`Self::shape_line`], this must be used only for single-line text (no `\n`). + pub fn shape_line_by_hash( + &self, + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + materialize_text: impl FnOnce() -> SharedString, + ) -> ShapedLine { + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run.len as u32; + continue; + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + } + + let mut used_force_width = force_width; + let layout = self.layout_line_by_hash( + text_hash, + text_len, + font_size, + runs, + used_force_width, + || { + let text = materialize_text(); + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + text + }, + ); + + // We only materialize actual text on cache miss; on hit we avoid allocations. + // Since `ShapedLine` carries a `SharedString`, use an empty placeholder for hits. + // NOTE: Callers must not rely on `ShapedLine.text` for content when using this API. + let text: SharedString = SharedString::new_static(""); + + ShapedLine { + layout, + text, + decoration_runs, + } + } + + /// Shape a multi line string of text, at the given font_size, for painting to the screen. + /// Subsets of the text can be styled independently with the `runs` parameter. + /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width. + pub fn shape_text( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + wrap_width: Option, + line_clamp: Option, + ) -> Result> { + let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable(); + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + + let mut lines = SmallVec::new(); + let mut max_wrap_lines = line_clamp; + let mut wrapped_lines = 0; + + let mut process_line = |line_text: SharedString, line_start, line_end| { + font_runs.clear(); + + let mut decoration_runs = >::with_capacity(32); + let mut run_start = line_start; + while run_start < line_end { + let Some(run) = runs.peek_mut() else { + log::warn!("`TextRun`s do not cover the entire to be shaped text"); + break; + }; + + let run_len_within_line = cmp::min(line_end - run_start, run.len); + + let decoration_changed = if let Some(last_run) = decoration_runs.last_mut() + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + && last_run.background_color == run.background_color + { + last_run.len += run_len_within_line as u32; + false + } else { + decoration_runs.push(DecorationRun { + len: run_len_within_line as u32, + color: run.color, + background_color: run.background_color, + underline: run.underline, + strikethrough: run.strikethrough, + }); + true + }; + + let font_id = self.resolve_font(&run.font); + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && !decoration_changed + { + font_run.len += run_len_within_line; + } else { + font_runs.push(FontRun { + len: run_len_within_line, + font_id, + }); + } + + // Preserve the remainder of the run for the next line + run.len -= run_len_within_line; + if run.len == 0 { + runs.next(); + } + run_start += run_len_within_line; + } + + let layout = self.line_layout_cache.layout_wrapped_line( + &line_text, + font_size, + &font_runs, + wrap_width, + max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)), + ); + wrapped_lines += layout.wrap_boundaries.len(); + + lines.push(WrappedLine { + layout, + decoration_runs, + text: line_text, + }); + + // Skip `\n` character. + if let Some(run) = runs.peek_mut() { + run.len -= 1; + if run.len == 0 { + runs.next(); + } + } + }; + + let mut split_lines = text.split('\n'); + + // Special case single lines to prevent allocating a sharedstring + if let Some(first_line) = split_lines.next() + && let Some(second_line) = split_lines.next() + { + let mut line_start = 0; + process_line( + SharedString::new(first_line), + line_start, + line_start + first_line.len(), + ); + line_start += first_line.len() + '\n'.len_utf8(); + process_line( + SharedString::new(second_line), + line_start, + line_start + second_line.len(), + ); + for line_text in split_lines { + line_start += line_text.len() + '\n'.len_utf8(); + process_line( + SharedString::new(line_text), + line_start, + line_start + line_text.len(), + ); + } + } else { + let end = text.len(); + process_line(text, 0, end); + } + + self.font_runs_pool.lock().push(font_runs); + + Ok(lines) + } + + pub(crate) fn finish_frame(&self) { + self.line_layout_cache.finish_frame() + } + + /// Layout the given line of text, at the given font_size. + /// Subsets of the line can be styled independently with the `runs` parameter. + /// Generally, you should prefer to use [`Self::shape_line`] instead, which + /// can be painted directly. + pub fn layout_line( + &self, + text: &str, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + ) -> Arc { + let mut last_run = None::<&TextRun>; + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + font_runs.clear(); + + for run in runs.iter() { + let decoration_changed = if let Some(last_run) = last_run + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + // we do not consider differing background color relevant, as it does not affect glyphs + // && last_run.background_color == run.background_color + { + false + } else { + last_run = Some(run); + true + }; + + let font_id = self.resolve_font(&run.font); + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + } + + let layout = self.line_layout_cache.layout_line( + &SharedString::new(text), + font_size, + &font_runs, + force_width, + ); + + self.font_runs_pool.lock().push(font_runs); + + layout + } + + /// Returns the shaped layout width of for the given character, in the given font and size. + pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels { + let mut buffer = [0; 4]; + let buffer: &_ = ch.encode_utf8(&mut buffer); + self.line_layout_cache + .layout_line( + buffer, + font_size, + &[FontRun { + len: buffer.len(), + font_id, + }], + None, + ) + .width + } + + /// Returns the shaped layout width of an `em`. + pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels { + self.layout_width(font_id, font_size, 'm') + } + + /// Probe the line layout cache using a caller-provided content hash, without allocating. + /// + /// Returns `Some(layout)` if the layout is already cached in either the current frame + /// or the previous frame. Returns `None` if it is not cached. + /// + /// Contract (caller enforced): + /// - Same `text_hash` implies identical text content (collision risk accepted by caller). + /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). + pub fn try_layout_line_by_hash( + &self, + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + ) -> Option> { + let mut last_run = None::<&TextRun>; + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + font_runs.clear(); + + for run in runs.iter() { + let decoration_changed = if let Some(last_run) = last_run + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + // we do not consider differing background color relevant, as it does not affect glyphs + // && last_run.background_color == run.background_color + { + false + } else { + last_run = Some(run); + true + }; + + let font_id = self.resolve_font(&run.font); + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + } + + let layout = self.line_layout_cache.try_layout_line_by_hash( + text_hash, + text_len, + font_size, + &font_runs, + force_width, + ); + + self.font_runs_pool.lock().push(font_runs); + + layout + } + + /// Layout the given line of text using a caller-provided content hash as the cache key. + /// + /// This enables cache hits without materializing a contiguous `SharedString` for the text. + /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. + /// + /// Contract (caller enforced): + /// - Same `text_hash` implies identical text content (collision risk accepted by caller). + /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). + pub fn layout_line_by_hash( + &self, + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &[TextRun], + force_width: Option, + materialize_text: impl FnOnce() -> SharedString, + ) -> Arc { + let mut last_run = None::<&TextRun>; + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + font_runs.clear(); + + for run in runs.iter() { + let decoration_changed = if let Some(last_run) = last_run + && last_run.color == run.color + && last_run.underline == run.underline + && last_run.strikethrough == run.strikethrough + // we do not consider differing background color relevant, as it does not affect glyphs + // && last_run.background_color == run.background_color + { + false + } else { + last_run = Some(run); + true + }; + + let font_id = self.resolve_font(&run.font); + if let Some(font_run) = font_runs.last_mut() + && font_id == font_run.font_id + && !decoration_changed + { + font_run.len += run.len; + } else { + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + } + + let layout = self.line_layout_cache.layout_line_by_hash( + text_hash, + text_len, + font_size, + &font_runs, + force_width, + materialize_text, + ); + + self.font_runs_pool.lock().push(font_runs); + + layout + } +} + +#[derive(Hash, Eq, PartialEq)] +struct FontIdWithSize { + font_id: FontId, + font_size: Pixels, +} + +/// A handle into the text system, which can be used to compute the wrapped layout of text +pub struct LineWrapperHandle { + wrapper: Option, + text_system: Arc, +} + +impl Drop for LineWrapperHandle { + fn drop(&mut self) { + let mut state = self.text_system.wrapper_pool.lock(); + let wrapper = self.wrapper.take().unwrap(); + state + .get_mut(&FontIdWithSize { + font_id: wrapper.font_id, + font_size: wrapper.font_size, + }) + .unwrap() + .push(wrapper); + } +} + +impl Deref for LineWrapperHandle { + type Target = LineWrapper; + + fn deref(&self) -> &Self::Target { + self.wrapper.as_ref().unwrap() + } +} + +impl DerefMut for LineWrapperHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.wrapper.as_mut().unwrap() + } +} + +/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0, +/// with 400.0 as normal. +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)] +#[serde(transparent)] +pub struct FontWeight(pub f32); + +impl Display for FontWeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FontWeight { + fn from(weight: f32) -> Self { + FontWeight(weight) + } +} + +impl Default for FontWeight { + #[inline] + fn default() -> FontWeight { + FontWeight::NORMAL + } +} + +impl Hash for FontWeight { + fn hash(&self, state: &mut H) { + state.write_u32(u32::from_be_bytes(self.0.to_be_bytes())); + } +} + +impl Eq for FontWeight {} + +impl FontWeight { + /// Thin weight (100), the thinnest value. + pub const THIN: FontWeight = FontWeight(100.0); + /// Extra light weight (200). + pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0); + /// Light weight (300). + pub const LIGHT: FontWeight = FontWeight(300.0); + /// Normal (400). + pub const NORMAL: FontWeight = FontWeight(400.0); + /// Medium weight (500, higher than normal). + pub const MEDIUM: FontWeight = FontWeight(500.0); + /// Semibold weight (600). + pub const SEMIBOLD: FontWeight = FontWeight(600.0); + /// Bold weight (700). + pub const BOLD: FontWeight = FontWeight(700.0); + /// Extra-bold weight (800). + pub const EXTRA_BOLD: FontWeight = FontWeight(800.0); + /// Black weight (900), the thickest value. + pub const BLACK: FontWeight = FontWeight(900.0); + + /// All of the font weights, in order from thinnest to thickest. + pub const ALL: [FontWeight; 9] = [ + Self::THIN, + Self::EXTRA_LIGHT, + Self::LIGHT, + Self::NORMAL, + Self::MEDIUM, + Self::SEMIBOLD, + Self::BOLD, + Self::EXTRA_BOLD, + Self::BLACK, + ]; +} + +impl schemars::JsonSchema for FontWeight { + fn schema_name() -> std::borrow::Cow<'static, str> { + "FontWeight".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + use schemars::json_schema; + json_schema!({ + "type": "number", + "minimum": Self::THIN, + "maximum": Self::BLACK, + "default": Self::default(), + "description": "Font weight value between 100 (thin) and 900 (black)" + }) + } +} + +/// Allows italic or oblique faces to be selected. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)] +pub enum FontStyle { + /// A face that is neither italic not obliqued. + #[default] + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Display for FontStyle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Debug::fmt(self, f) + } +} + +/// A styled run of text, for use in [`crate::TextLayout`]. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct TextRun { + /// A number of utf8 bytes + pub len: usize, + /// The font to use for this run. + pub font: Font, + /// The color + pub color: Hsla, + /// The background color (if any) + pub background_color: Option, + /// The underline style (if any) + pub underline: Option, + /// The strikethrough style (if any) + pub strikethrough: Option, +} + +#[cfg(all(target_os = "macos", test))] +impl TextRun { + fn with_len(&self, len: usize) -> Self { + let mut this = self.clone(); + this.len = len; + this + } +} + +/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub struct GlyphId(pub u32); + +/// Parameters for rendering a glyph, used as cache keys for raster bounds. +/// +/// This struct identifies a specific glyph rendering configuration including +/// font, size, subpixel positioning, and scale factor. It's used to look up +/// cached raster bounds and sprite atlas entries. +#[derive(Clone, Debug, PartialEq)] +#[expect(missing_docs)] +pub struct RenderGlyphParams { + pub font_id: FontId, + pub glyph_id: GlyphId, + pub font_size: Pixels, + pub subpixel_variant: Point, + pub scale_factor: f32, + pub is_emoji: bool, + pub subpixel_rendering: bool, + pub dilation: u8, +} + +impl Eq for RenderGlyphParams {} + +impl Hash for RenderGlyphParams { + fn hash(&self, state: &mut H) { + self.font_id.0.hash(state); + self.glyph_id.0.hash(state); + self.font_size.0.to_bits().hash(state); + self.subpixel_variant.hash(state); + self.scale_factor.to_bits().hash(state); + self.is_emoji.hash(state); + self.subpixel_rendering.hash(state); + self.dilation.hash(state); + } +} + +/// The configuration details for identifying a specific font. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Font { + /// The font family name. + /// + /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform. + pub family: SharedString, + + /// The font features to use. + pub features: FontFeatures, + + /// The fallbacks fonts to use. + pub fallbacks: Option, + + /// The font weight. + pub weight: FontWeight, + + /// The font style. + pub style: FontStyle, +} + +impl Default for Font { + fn default() -> Self { + font(".SystemUIFont") + } +} + +/// Get a [`Font`] for a given name. +pub fn font(family: impl Into) -> Font { + Font { + family: family.into(), + features: FontFeatures::default(), + weight: FontWeight::default(), + style: FontStyle::default(), + fallbacks: None, + } +} + +impl Font { + /// Set this Font to be bold + pub fn bold(mut self) -> Self { + self.weight = FontWeight::BOLD; + self + } + + /// Set this Font to be italic + pub fn italic(mut self) -> Self { + self.style = FontStyle::Italic; + self + } +} + +/// A struct for storing font metrics. +/// It is used to define the measurements of a typeface. +#[derive(Clone, Copy, Debug)] +pub struct FontMetrics { + /// The number of font units that make up the "em square", + /// a scalable grid for determining the size of a typeface. + pub units_per_em: u32, + + /// The vertical distance from the baseline of the font to the top of the glyph covers. + pub ascent: f32, + + /// The vertical distance from the baseline of the font to the bottom of the glyph covers. + pub descent: f32, + + /// The recommended additional space to add between lines of type. + pub line_gap: f32, + + /// The suggested position of the underline. + pub underline_position: f32, + + /// The suggested thickness of the underline. + pub underline_thickness: f32, + + /// The height of a capital letter measured from the baseline of the font. + pub cap_height: f32, + + /// The height of a lowercase x. + pub x_height: f32, + + /// The outer limits of the area that the font covers. + /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table + pub bounding_box: Bounds, +} + +impl FontMetrics { + /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels. + pub fn ascent(&self, font_size: Pixels) -> Pixels { + Pixels((self.ascent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels. + pub fn descent(&self, font_size: Pixels) -> Pixels { + Pixels((self.descent / self.units_per_em as f32) * font_size.0) + } + + /// Returns the recommended additional space to add between lines of type in pixels. + pub fn line_gap(&self, font_size: Pixels) -> Pixels { + Pixels((self.line_gap / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested position of the underline in pixels. + pub fn underline_position(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_position / self.units_per_em as f32) * font_size.0) + } + + /// Returns the suggested thickness of the underline in pixels. + pub fn underline_thickness(&self, font_size: Pixels) -> Pixels { + Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a capital letter measured from the baseline of the font in pixels. + pub fn cap_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.cap_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the height of a lowercase x in pixels. + pub fn x_height(&self, font_size: Pixels) -> Pixels { + Pixels((self.x_height / self.units_per_em as f32) * font_size.0) + } + + /// Returns the outer limits of the area that the font covers in pixels. + pub fn bounding_box(&self, font_size: Pixels) -> Bounds { + (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) + } +} + +/// Maps well-known virtual font names to their concrete equivalents. +#[allow(unused)] +pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { + // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" + // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, + // and so retained here for backward compatibility. + match name { + ".SystemUIFont" => system, + ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", + ".ZedMono" | "Zed Plex Mono" => "Lilex", + _ => name, + } +} + +/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references. +#[allow(unused)] +pub fn font_name_with_fallbacks_shared<'a>( + name: &'a SharedString, + system: &'a SharedString, +) -> &'a SharedString { + // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" + // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, + // and so retained here for backward compatibility. + match name.as_str() { + ".SystemUIFont" => system, + ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") }, + ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") }, + _ => name, + } +} + +#[cfg(test)] +mod missing_glyph_tests { + use super::*; + use futures::FutureExt as _; + + #[test] + fn bounds_retained_missing_glyphs() { + let (reporter, mut receiver) = missing_glyph_channel(); + reporter.report( + (0..MAX_REPORTED_MISSING_GLYPHS) + .map(|index| { + MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional) + }) + .collect(), + ); + assert!(receiver.recv().now_or_never().unwrap().is_ok()); + + let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace); + reporter.report(vec![newest.clone()]); + assert!(receiver.recv().now_or_never().unwrap().is_ok()); + + let state = &receiver.state; + assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS); + assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS); + assert!(state.reported.contains(&newest)); + } + + #[test] + fn dropping_receiver_closes_and_clears_reports() { + let (reporter, receiver) = missing_glyph_channel(); + reporter.report(vec![missing_glyph("missing")]); + + drop(receiver); + + assert!(reporter.sender.is_closed()); + assert!(reporter.sender.is_empty()); + } + + fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) { + let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS); + let generation = Arc::::default(); + let reporter = MissingGlyphReporter { + generation: generation.clone(), + sender, + }; + let receiver = MissingGlyphReceiver { + state: MissingGlyphState::default(), + generation, + receiver, + }; + (reporter, receiver) + } + + fn missing_glyph(grapheme: &'static str) -> MissingGlyph { + MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional) + } +} diff --git a/vendor/gpui-pre/src/text_system/font_fallbacks.rs b/vendor/gpui-pre/src/text_system/font_fallbacks.rs new file mode 100644 index 0000000..63dc89b --- /dev/null +++ b/vendor/gpui-pre/src/text_system/font_fallbacks.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// The fallback fonts that can be configured for a given font. +/// Fallback fonts family names are stored here. +#[derive(Default, Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize, JsonSchema)] +pub struct FontFallbacks(pub Arc>); + +impl FontFallbacks { + /// Get the fallback fonts family names + pub fn fallback_list(&self) -> &[String] { + self.0.as_slice() + } + + /// Create a font fallback from a list of strings + pub fn from_fonts(fonts: Vec) -> Self { + FontFallbacks(Arc::new(fonts)) + } +} diff --git a/vendor/gpui-pre/src/text_system/font_features.rs b/vendor/gpui-pre/src/text_system/font_features.rs new file mode 100644 index 0000000..c1ab72b --- /dev/null +++ b/vendor/gpui-pre/src/text_system/font_features.rs @@ -0,0 +1,154 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use schemars::{JsonSchema, json_schema}; + +/// The OpenType features that can be configured for a given font. +#[derive(Default, Clone, Eq, PartialEq, Hash)] +pub struct FontFeatures(pub Arc>); + +impl FontFeatures { + /// Disables `calt`. + pub fn disable_ligatures() -> Self { + Self(Arc::new(vec![("calt".into(), 0)])) + } + + /// Get the tag name list of the font OpenType features + /// only enabled or disabled features are returned + pub fn tag_value_list(&self) -> &[(String, u32)] { + self.0.as_slice() + } + + /// Returns whether the `calt` feature is enabled. + /// + /// Returns `None` if the feature is not present. + pub fn is_calt_enabled(&self) -> Option { + self.0 + .iter() + .find(|(feature, _)| feature == "calt") + .map(|(_, value)| *value == 1) + } +} + +impl std::fmt::Debug for FontFeatures { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_struct("FontFeatures"); + for (tag, value) in self.tag_value_list() { + debug.field(tag, value); + } + + debug.finish() + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum FeatureValue { + Bool(bool), + Number(serde_json::Number), +} + +impl<'de> serde::Deserialize<'de> for FontFeatures { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{MapAccess, Visitor}; + use std::fmt; + + struct FontFeaturesVisitor; + + impl<'de> Visitor<'de> for FontFeaturesVisitor { + type Value = FontFeatures; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map of font features") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut feature_list = Vec::new(); + + while let Some((key, value)) = + access.next_entry::>()? + { + if !is_valid_feature_tag(&key) { + log::error!("Incorrect font feature tag: {}", key); + continue; + } + if let Some(value) = value { + match value { + FeatureValue::Bool(enable) => { + if enable { + feature_list.push((key, 1)); + } else { + feature_list.push((key, 0)); + } + } + FeatureValue::Number(value) => { + if value.is_u64() { + feature_list.push((key, value.as_u64().unwrap() as u32)); + } else { + log::error!( + "Incorrect font feature value {} for feature tag {}", + value, + key + ); + continue; + } + } + } + } + } + + Ok(FontFeatures(Arc::new(feature_list))) + } + } + + let features = deserializer.deserialize_map(FontFeaturesVisitor)?; + Ok(features) + } +} + +impl serde::Serialize for FontFeatures { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(None)?; + + for (tag, value) in self.tag_value_list() { + map.serialize_entry(tag, value)?; + } + + map.end() + } +} + +impl JsonSchema for FontFeatures { + fn schema_name() -> Cow<'static, str> { + "FontFeatures".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + json_schema!({ + "type": "object", + "patternProperties": { + "[0-9a-zA-Z]{4}$": { + "type": ["boolean", "integer"], + "minimum": 0, + "multipleOf": 1 + } + }, + "additionalProperties": false + }) + } +} + +fn is_valid_feature_tag(tag: &str) -> bool { + tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric()) +} diff --git a/vendor/gpui-pre/src/text_system/line.rs b/vendor/gpui-pre/src/text_system/line.rs new file mode 100644 index 0000000..f521005 --- /dev/null +++ b/vendor/gpui-pre/src/text_system/line.rs @@ -0,0 +1,1979 @@ +use crate::{ + App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result, + SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window, WrapBoundary, + WrappedLineLayout, black, fill, point, px, size, underline_y_offset, +}; +use derive_more::{Deref, DerefMut}; +use smallvec::SmallVec; +use std::{ops::Range, sync::Arc}; + +/// Pre-computed glyph data for efficient painting without per-glyph cache lookups. +/// +/// This is produced by `ShapedLine::compute_glyph_raster_data` during prepaint +/// and consumed by `ShapedLine::paint_with_raster_data` during paint. +#[derive(Clone, Debug)] +pub struct GlyphRasterData { + /// The raster bounds for each glyph, in paint order. + pub bounds: Vec>, + /// The render params for each glyph (needed for sprite atlas lookup). + pub params: Vec, +} + +/// Set the text decoration for a run of text. +#[derive(Debug, Clone)] +pub struct DecorationRun { + /// The length of the run in utf-8 bytes. + pub len: u32, + + /// The color for this run + pub color: Hsla, + + /// The background color for this run + pub background_color: Option, + + /// The underline style for this run + pub underline: Option, + + /// The strikethrough style for this run + pub strikethrough: Option, +} + +/// A line of text that has been shaped and decorated. +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct ShapedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} + +impl ShapedLine { + /// Returns a forward-only cursor for this shaped line. + pub fn cursor(&self) -> ShapedLineCursor<'_> { + assert_eq!( + self.len(), + self.text.len(), + "cannot split a shaped line with an adjusted length" + ); + let byte_ordered = self + .layout + .runs + .iter() + .flat_map(|run| run.glyphs.iter().map(|glyph| glyph.index)) + .is_sorted(); + ShapedLineCursor { + line: self, + unordered_remainder: (!byte_ordered).then(|| self.clone()), + byte_index: 0, + run_index: 0, + glyph_index: 0, + decoration_index: 0, + decoration_offset: 0, + x_offset: px(0.), + } + } + + /// The length of the line in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len + } + + /// The width of the shaped line in pixels. + /// + /// This is the glyph advance width computed by the text shaping system and is useful for + /// incrementally advancing a "pen" when painting multiple fragments on the same row. + pub fn width(&self) -> Pixels { + self.layout.width + } + + /// Override the len, useful if you're rendering text a + /// as text b (e.g. rendering invisibles). + pub fn with_len(mut self, len: usize) -> Self { + let layout = self.layout.as_ref(); + self.layout = Arc::new(LineLayout { + font_size: layout.font_size, + width: layout.width, + ascent: layout.ascent, + descent: layout.descent, + runs: layout.runs.clone(), + len, + }); + self + } + + /// Paint the line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + align_width: Option, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + self.paint_with_underline_handler( + origin, + line_height, + align, + align_width, + window, + cx, + |_, origin, width, style, window| window.paint_underline(origin, width, style), + ) + } + + /// Paint the line with a handler for each underline. + pub fn paint_with_underline_handler( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + align_width: Option, + window: &mut Window, + cx: &mut App, + mut paint_underline: impl FnMut( + Range, + Point, + Pixels, + &UnderlineStyle, + &mut Window, + ), + ) -> Result<()> { + paint_line( + origin, + &self.layout, + line_height, + align, + align_width, + &self.decoration_runs, + &[], + window, + cx, + &mut paint_underline, + ) + } + + /// Paint the background of the line to the window. + pub fn paint_background( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + align_width: Option, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + paint_line_background( + origin, + &self.layout, + line_height, + align, + align_width, + &self.decoration_runs, + &[], + window, + cx, + )?; + + Ok(()) + } + + /// Split this shaped line at a byte index, returning `(prefix, suffix)`. + /// + /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions. + /// Its width equals the x-advance up to the split point. + /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions + /// shifted left so the first glyph starts at x=0, and byte indices rebased to 0. + /// - Decoration runs are partitioned at the boundary; a run that straddles it is + /// split into two with adjusted lengths. + /// - `font_size`, `ascent`, and `descent` are copied to both halves. + pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) { + let (left_layout, right_layout) = self.layout.split_at(byte_index); + + // Partition decoration runs. A run straddling the boundary is split into two. + let mut left_decorations = SmallVec::new(); + let mut right_decorations = SmallVec::new(); + let mut decoration_offset = 0u32; + let split_point = byte_index as u32; + + for decoration in &self.decoration_runs { + let run_end = decoration_offset + decoration.len; + + if run_end <= split_point { + left_decorations.push(decoration.clone()); + } else if decoration_offset >= split_point { + right_decorations.push(decoration.clone()); + } else { + let left_len = split_point - decoration_offset; + let right_len = run_end - split_point; + left_decorations.push(DecorationRun { + len: left_len, + color: decoration.color, + background_color: decoration.background_color, + underline: decoration.underline, + strikethrough: decoration.strikethrough, + }); + right_decorations.push(DecorationRun { + len: right_len, + color: decoration.color, + background_color: decoration.background_color, + underline: decoration.underline, + strikethrough: decoration.strikethrough, + }); + } + + decoration_offset = run_end; + } + + // Split text + let left_text = if byte_index == self.text.len() { + self.text.clone() + } else { + SharedString::new(&self.text[..byte_index]) + }; + let right_text = if byte_index == 0 { + self.text.clone() + } else { + SharedString::new(&self.text[byte_index..]) + }; + + let left = ShapedLine { + layout: Arc::new(left_layout), + text: left_text, + decoration_runs: left_decorations, + }; + + let right = ShapedLine { + layout: Arc::new(right_layout), + text: right_text, + decoration_runs: right_decorations, + }; + + (left, right) + } +} + +/// Incrementally splits a [`ShapedLine`] at increasing UTF-8 byte boundaries. +/// +/// Each piece preserves the original glyphs and decorations, rebased as in +/// [`ShapedLine::split_at`]. Byte-ordered glyphs are advanced in linear time, +/// copying each glyph and byte at most once; visually reordered glyphs fall +/// back to the existing split operation. +pub struct ShapedLineCursor<'a> { + line: &'a ShapedLine, + /// Bidirectional shaping can put glyphs out of byte order. + unordered_remainder: Option, + byte_index: usize, + run_index: usize, + glyph_index: usize, + decoration_index: usize, + decoration_offset: u32, + x_offset: Pixels, +} + +impl<'a> ShapedLineCursor<'a> { + /// Takes the bytes since the previous boundary. + /// + /// Panics if the boundary precedes the previous one, exceeds the line's + /// length, or falls inside a UTF-8 character. + pub fn take_until(&mut self, byte_index: usize) -> ShapedLine { + assert!( + byte_index >= self.byte_index, + "split boundary moved backwards" + ); + assert!( + byte_index <= self.line.len(), + "split boundary exceeds line length" + ); + assert!( + self.line.text.is_char_boundary(byte_index), + "split boundary is not a UTF-8 character boundary" + ); + let previous_index = self.byte_index; + let previous_x = self.x_offset; + if let Some(remainder) = &mut self.unordered_remainder { + let (piece, rest) = remainder.split_at(byte_index - previous_index); + *remainder = rest; + self.byte_index = byte_index; + self.x_offset = self.line.layout.x_for_index(byte_index); + return piece; + } + let mut runs = Vec::new(); + let mut next_x = self.line.layout.width; + while let Some(run) = self.line.layout.runs.get(self.run_index) { + let start = self.glyph_index; + while let Some(glyph) = run.glyphs.get(self.glyph_index) { + if glyph.index >= byte_index { + break; + } + self.glyph_index += 1; + } + let end = self.glyph_index; + if start < end { + runs.push(crate::ShapedRun { + font_id: run.font_id, + glyphs: run.glyphs[start..end] + .iter() + .map(|glyph| crate::ShapedGlyph { + id: glyph.id, + position: point(glyph.position.x - previous_x, glyph.position.y), + index: glyph.index - previous_index, + is_emoji: glyph.is_emoji, + }) + .collect(), + }); + } + if let Some(glyph) = run.glyphs.get(self.glyph_index) { + next_x = glyph.position.x; + break; + } + self.run_index += 1; + self.glyph_index = 0; + } + let mut decorations = SmallVec::new(); + while let Some(decoration) = self.line.decoration_runs.get(self.decoration_index) + && (self.decoration_offset < byte_index as u32 + || (decoration.len == 0 && self.decoration_offset == byte_index as u32)) + { + let end = self.decoration_offset + decoration.len; + let start = self.decoration_offset.max(previous_index as u32); + let len = end.min(byte_index as u32) - start; + if len > 0 || decoration.len == 0 { + decorations.push(DecorationRun { + len, + color: decoration.color, + background_color: decoration.background_color, + underline: decoration.underline, + strikethrough: decoration.strikethrough, + }); + } + if end <= byte_index as u32 { + self.decoration_index += 1; + self.decoration_offset = end; + } else { + break; + } + } + self.byte_index = byte_index; + self.x_offset = next_x; + ShapedLine { + layout: Arc::new(LineLayout { + font_size: self.line.layout.font_size, + width: next_x - previous_x, + ascent: self.line.layout.ascent, + descent: self.line.layout.descent, + runs, + len: byte_index - previous_index, + }), + text: SharedString::new(&self.line.text[previous_index..byte_index]), + decoration_runs: decorations, + } + } + + /// Returns the original line's x position at the current boundary. + pub fn x_offset(&self) -> Pixels { + self.x_offset + } +} + +impl LineLayout { + /// Paint this layout to the window, using the given decoration runs to color + /// glyphs and draw underlines and strikethroughs. + /// + /// This is a lower-level alternative to [`ShapedLine::paint`] for callers that + /// hold a bare layout and track decorations themselves. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + paint_line( + origin, + self, + line_height, + align, + align_width, + decoration_runs, + &[], + window, + cx, + &mut |_, origin, width, style, window| window.paint_underline(origin, width, style), + ) + } + + /// Paint the background of this layout to the window, using the given + /// decoration runs to determine background colors. + /// + /// This is a lower-level alternative to [`ShapedLine::paint_background`] for + /// callers that hold a bare layout and track decorations themselves. + pub fn paint_background( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + paint_line_background( + origin, + self, + line_height, + align, + align_width, + decoration_runs, + &[], + window, + cx, + ) + } +} + +/// A line of text that has been shaped, decorated, and wrapped by the text layout system. +#[derive(Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + /// The text that was shaped for this line. + pub text: SharedString, + pub(crate) decoration_runs: Vec, +} + +impl WrappedLine { + /// The length of the underlying, unwrapped layout, in utf-8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.layout.len() + } + + /// Paint this line of text to the window. + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + bounds: Option>, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + let align_width = match bounds { + Some(bounds) => Some(bounds.size.width), + None => self.layout.wrap_width, + }; + + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + &mut |_, origin, width, style, window| window.paint_underline(origin, width, style), + )?; + + Ok(()) + } + + /// Paint the background of line of text to the window. + pub fn paint_background( + &self, + origin: Point, + line_height: Pixels, + align: TextAlign, + bounds: Option>, + window: &mut Window, + cx: &mut App, + ) -> Result<()> { + let align_width = match bounds { + Some(bounds) => Some(bounds.size.width), + None => self.layout.wrap_width, + }; + + paint_line_background( + origin, + &self.layout.unwrapped_layout, + line_height, + align, + align_width, + &self.decoration_runs, + &self.wrap_boundaries, + window, + cx, + )?; + + Ok(()) + } +} + +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + wrap_boundaries: &[WrapBoundary], + window: &mut Window, + cx: &mut App, + paint_underline: &mut dyn FnMut( + Range, + Point, + Pixels, + &UnderlineStyle, + &mut Window, + ), +) -> Result<()> { + let line_bounds = line_paint_bounds( + origin, + layout, + line_height, + align, + align_width, + wrap_boundaries, + ); + window.paint_layer(line_bounds, |window| { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let underline_y_offset = underline_y_offset(line_height, layout.ascent, layout.descent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle, Range)> = None; + let mut current_strikethrough: Option<(Point, StrikethroughStyle)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = point( + aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + px(0.0), + &align, + layout, + wraps.peek(), + ), + origin.y, + ); + let mut prev_glyph_position = Point::default(); + let mut max_glyph_size = size(px(0.), px(0.)); + let mut first_glyph_x = origin.x; + for (run_ix, run) in layout.runs.iter().enumerate() { + max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + if glyph_ix == 0 && run_ix == 0 { + first_glyph_x = glyph_origin.x; + } + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((underline_origin, underline_style, underline_range)) = + current_underline.as_mut() + { + if glyph_origin.x == underline_origin.x { + underline_origin.x -= max_glyph_size.width.half(); + }; + paint_underline( + underline_range.clone(), + *underline_origin, + glyph_origin.x - underline_origin.x, + underline_style, + window, + ); + if glyph.index < run_end { + underline_origin.x = origin.x; + underline_origin.y += line_height; + } else { + current_underline = None; + } + } + if let Some((strikethrough_origin, strikethrough_style)) = + current_strikethrough.as_mut() + { + if glyph_origin.x == strikethrough_origin.x { + strikethrough_origin.x -= max_glyph_size.width.half(); + }; + window.paint_strikethrough( + *strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + strikethrough_style, + ); + if glyph.index < run_end { + strikethrough_origin.x = origin.x; + strikethrough_origin.y += line_height; + } else { + current_strikethrough = None; + } + } + + glyph_origin.x = aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + glyph.position.x, + &align, + layout, + wraps.peek(), + ); + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_underline: Option<(Point, UnderlineStyle, Range)> = + None; + let mut finished_strikethrough: Option<(Point, StrikethroughStyle)> = None; + if glyph.index >= run_end { + let mut style_run = decoration_runs.next(); + + // ignore style runs that apply to a partial glyph + while let Some(run) = style_run { + if glyph.index < run_end + (run.len as usize) { + break; + } + run_end += run.len as usize; + style_run = decoration_runs.next(); + } + + if let Some(style_run) = style_run { + let style_run_start = run_end; + if let Some((_, underline_style, underline_range)) = &mut current_underline + { + if style_run.underline.as_ref() != Some(underline_style) { + finished_underline = current_underline.take(); + } else { + underline_range.end = style_run_start + style_run.len as usize; + } + } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point(glyph_origin.x, glyph_origin.y + underline_y_offset), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + style_run_start..style_run_start + style_run.len as usize, + )); + } + if let Some((_, strikethrough_style)) = &mut current_strikethrough + && style_run.strikethrough.as_ref() != Some(strikethrough_style) + { + finished_strikethrough = current_strikethrough.take(); + } + if let Some(run_strikethrough) = style_run.strikethrough.as_ref() { + current_strikethrough.get_or_insert(( + point( + glyph_origin.x, + glyph_origin.y + + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5), + ), + StrikethroughStyle { + color: Some(run_strikethrough.color.unwrap_or(style_run.color)), + thickness: run_strikethrough.thickness, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_underline = current_underline.take(); + finished_strikethrough = current_strikethrough.take(); + } + } + + if let Some((mut underline_origin, underline_style, underline_range)) = + finished_underline + { + if underline_origin.x == glyph_origin.x { + underline_origin.x -= max_glyph_size.width.half(); + }; + paint_underline( + underline_range, + underline_origin, + glyph_origin.x - underline_origin.x, + &underline_style, + window, + ); + } + + if let Some((mut strikethrough_origin, strikethrough_style)) = + finished_strikethrough + { + if strikethrough_origin.x == glyph_origin.x { + strikethrough_origin.x -= max_glyph_size.width.half(); + }; + window.paint_strikethrough( + strikethrough_origin, + glyph_origin.x - strikethrough_origin.x, + &strikethrough_style, + ); + } + + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; + + let content_mask = window.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + let vertical_offset = point(px(0.0), glyph.position.y); + if glyph.is_emoji { + window.paint_emoji( + glyph_origin + baseline_offset + vertical_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + window.paint_glyph( + glyph_origin + baseline_offset + vertical_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; + } + } + } + } + + let mut last_line_end_x = first_glyph_x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((mut underline_start, underline_style, underline_range)) = + current_underline.take() + { + if last_line_end_x == underline_start.x { + underline_start.x -= max_glyph_size.width.half() + }; + paint_underline( + underline_range, + underline_start, + last_line_end_x - underline_start.x, + &underline_style, + window, + ); + } + + if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() { + if last_line_end_x == strikethrough_start.x { + strikethrough_start.x -= max_glyph_size.width.half() + }; + window.paint_strikethrough( + strikethrough_start, + last_line_end_x - strikethrough_start.x, + &strikethrough_style, + ); + } + + Ok(()) + }) +} + +fn paint_line_background( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: TextAlign, + align_width: Option, + decoration_runs: &[DecorationRun], + wrap_boundaries: &[WrapBoundary], + window: &mut Window, + cx: &mut App, +) -> Result<()> { + let line_bounds = line_paint_bounds( + origin, + layout, + line_height, + align, + align_width, + wrap_boundaries, + ); + window.paint_layer(line_bounds, |window| { + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut current_background: Option<(Point, Hsla)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = point( + aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + px(0.0), + &align, + layout, + wraps.peek(), + ), + origin.y, + ); + let mut prev_glyph_position = Point::default(); + let mut max_glyph_size = size(px(0.), px(0.)); + for (run_ix, run) in layout.runs.iter().enumerate() { + max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size; + + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; + + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((background_origin, background_color)) = current_background.as_mut() + { + if glyph_origin.x == background_origin.x { + background_origin.x -= max_glyph_size.width.half() + } + window.paint_quad(fill( + Bounds { + origin: *background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + *background_color, + )); + if glyph.index < run_end { + background_origin.x = origin.x; + background_origin.y += line_height; + } else { + current_background = None; + } + } + + glyph_origin.x = aligned_origin_x( + origin, + align_width.unwrap_or(layout.width), + glyph.position.x, + &align, + layout, + wraps.peek(), + ); + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; + + let mut finished_background: Option<(Point, Hsla)> = None; + if glyph.index >= run_end { + let mut style_run = decoration_runs.next(); + + // ignore style runs that apply to a partial glyph + while let Some(run) = style_run { + if glyph.index < run_end + (run.len as usize) { + break; + } + run_end += run.len as usize; + style_run = decoration_runs.next(); + } + + if let Some(style_run) = style_run { + if let Some((_, background_color)) = &mut current_background + && style_run.background_color.as_ref() != Some(background_color) + { + finished_background = current_background.take(); + } + if let Some(run_background) = style_run.background_color { + current_background.get_or_insert(( + point(glyph_origin.x, glyph_origin.y), + run_background, + )); + } + run_end += style_run.len as usize; + } else { + run_end = layout.len; + finished_background = current_background.take(); + } + } + + if let Some((mut background_origin, background_color)) = finished_background { + let mut width = glyph_origin.x - background_origin.x; + if background_origin.x == glyph_origin.x { + background_origin.x -= max_glyph_size.width.half(); + }; + window.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(width, line_height), + }, + background_color, + )); + } + } + } + + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((mut background_origin, background_color)) = current_background.take() { + if last_line_end_x == background_origin.x { + background_origin.x -= max_glyph_size.width.half() + }; + window.paint_quad(fill( + Bounds { + origin: background_origin, + size: size(last_line_end_x - background_origin.x, line_height), + }, + background_color, + )); + } + + Ok(()) + }) +} + +fn line_paint_bounds( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + align: TextAlign, + align_width: Option, + wrap_boundaries: &[WrapBoundary], +) -> Bounds { + let mut bounds = Bounds::new( + origin, + size( + layout.width, + line_height * (wrap_boundaries.len() as f32 + 1.), + ), + ); + if align == TextAlign::Left || layout.len == 0 { + return bounds; + } + + let align_width = align_width.unwrap_or(layout.width); + let mut row_start = Pixels::ZERO; + let row_ends = wrap_boundaries + .iter() + .map(|boundary| { + layout.runs[boundary.run_ix].glyphs[boundary.glyph_ix] + .position + .x + }) + .chain([layout.width]); + for (row, row_end) in row_ends.enumerate() { + let width = row_end - row_start; + let offset = match align { + TextAlign::Left => Pixels::ZERO, + TextAlign::Center => (align_width - width) / 2., + TextAlign::Right => align_width - width, + }; + bounds = bounds.union(&Bounds::new( + point(origin.x + offset, origin.y + line_height * row as f32), + size(width, line_height), + )); + row_start = row_end; + } + bounds +} + +fn aligned_origin_x( + origin: Point, + align_width: Pixels, + last_glyph_x: Pixels, + align: &TextAlign, + layout: &LineLayout, + wrap_boundary: Option<&&WrapBoundary>, +) -> Pixels { + let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary { + layout.runs[*run_ix].glyphs[*glyph_ix].position.x + } else { + layout.width + }; + + let line_width = end_of_line - last_glyph_x; + + match align { + TextAlign::Left => origin.x, + TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0, + TextAlign::Right => origin.x + align_width - line_width, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AppContext as _, Context, FontId, GlyphId, IntoElement, Render, ShapedGlyph, ShapedRun, + Styled, TestAppContext, TextRun, Underline, canvas, font, hsla, + }; + use std::rc::Rc; + + /// Helper: build a ShapedLine from glyph descriptors without the platform text system. + /// Each glyph is described as (byte_index, x_position). + fn make_shaped_line( + text: &str, + glyphs: &[(usize, f32)], + width: f32, + decorations: &[DecorationRun], + ) -> ShapedLine { + let shaped_glyphs: Vec = glyphs + .iter() + .map(|&(index, x)| ShapedGlyph { + id: GlyphId(0), + position: point(px(x), px(0.0)), + index, + is_emoji: false, + }) + .collect(); + + ShapedLine { + layout: Arc::new(LineLayout { + font_size: px(16.0), + width: px(width), + ascent: px(12.0), + descent: px(4.0), + runs: vec![ShapedRun { + font_id: FontId(0), + glyphs: shaped_glyphs, + }], + len: text.len(), + }), + text: SharedString::new(text), + decoration_runs: SmallVec::from(decorations.to_vec()), + } + } + + #[test] + fn test_aligned_line_paint_bounds() { + let line = make_shaped_line("abcd", &[(0, 0.), (1, 10.), (2, 20.), (3, 30.)], 40., &[]); + let origin = point(px(10.), px(20.)); + let line_height = px(16.); + for (wrapped, align_width, expected_edges) in [ + (false, None, [(10., 50.), (10., 50.), (10., 50.)]), + (true, None, [(10., 50.), (10., 50.), (10., 50.)]), + (false, Some(40.), [(10., 50.), (10., 50.), (10., 50.)]), + (true, Some(40.), [(10., 50.), (10., 50.), (10., 50.)]), + (false, Some(100.), [(10., 50.), (10., 80.), (10., 110.)]), + (true, Some(100.), [(10., 50.), (10., 70.), (10., 110.)]), + (false, Some(10.), [(10., 50.), (-5., 50.), (-20., 50.)]), + (true, Some(10.), [(10., 50.), (5., 50.), (0., 50.)]), + (false, Some(0.), [(10., 50.), (-10., 50.), (-30., 50.)]), + (true, Some(0.), [(10., 50.), (0., 50.), (-10., 50.)]), + ] { + let boundaries = if wrapped { + vec![WrapBoundary { + run_ix: 0, + glyph_ix: 2, + }] + } else { + Vec::new() + }; + for (align, (left, right)) in [TextAlign::Left, TextAlign::Center, TextAlign::Right] + .into_iter() + .zip(expected_edges) + { + assert_eq!( + line_paint_bounds( + origin, + &line.layout, + line_height, + align, + align_width.map(px), + &boundaries, + ), + Bounds::new( + point(px(left), origin.y), + size(px(right - left), px(if wrapped { 32. } else { 16. })), + ), + "wrapped={wrapped}, align={align:?}, align_width={align_width:?}", + ); + } + } + let empty = make_shaped_line("", &[], 0., &[]); + for align in [TextAlign::Left, TextAlign::Center, TextAlign::Right] { + assert_eq!( + line_paint_bounds( + origin, + &empty.layout, + line_height, + align, + Some(px(100.)), + &[] + ), + Bounds::new(origin, size(Pixels::ZERO, line_height)), + ); + } + } + + #[gpui::test] + fn test_underline_handler_matches_default_paint(cx: &mut TestAppContext) { + test_underline_handler_at_scales(cx, |window, cx| { + let first_style = UnderlineStyle { + thickness: px(1.), + color: Some(hsla(0., 1., 0.5, 1.)), + wavy: true, + }; + let last_style = UnderlineStyle { + color: Some(hsla(0.5, 1., 0.5, 1.)), + wavy: false, + ..first_style + }; + let fallback_style = UnderlineStyle { + color: Some(black()), + ..first_style + }; + let decoration = DecorationRun { + len: 1, + color: black(), + background_color: None, + underline: Some(first_style), + strikethrough: None, + }; + let line = underline_test_line( + "aébcde", + &[ + decoration.clone(), + DecorationRun { + len: 2, + ..decoration.clone() + }, + DecorationRun { + underline: None, + ..decoration.clone() + }, + DecorationRun { + underline: Some(UnderlineStyle { + color: None, + ..first_style + }), + ..decoration.clone() + }, + DecorationRun { + underline: Some(last_style), + ..decoration.clone() + }, + DecorationRun { + underline: Some(last_style), + ..decoration + }, + ], + false, + window, + ); + assert_eq!(line.width(), px(48.)); + for origin_x in [-3.25, 0., 4.25] { + for (align, align_width, offset) in [ + (TextAlign::Left, None, 0.), + (TextAlign::Center, None, 0.), + (TextAlign::Right, None, 0.), + (TextAlign::Left, Some(px(96.)), 0.), + (TextAlign::Center, Some(px(96.)), 24.), + (TextAlign::Right, Some(px(96.)), 48.), + ] { + let origin = point(px(origin_x), px(12.25)); + let line_height = px(20.); + window.next_frame.scene.clear(); + line.paint(origin, line_height, align, align_width, window, cx) + .unwrap(); + let original = window.next_frame.scene.underlines.clone(); + window.next_frame.scene.clear(); + line.layout + .paint( + origin, + line_height, + align, + align_width, + &line.decoration_runs, + window, + cx, + ) + .unwrap(); + assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original); + + window.next_frame.scene.clear(); + let mut strokes = Vec::new(); + line.paint_with_underline_handler( + origin, + line_height, + align, + align_width, + window, + cx, + |range, origin, width, style, window| { + strokes.push((range, origin, width, *style)); + window.paint_underline(origin, width, style); + }, + ) + .unwrap(); + let start = px(origin_x + offset); + let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent); + assert_eq!( + strokes, + [ + (0..3, point(start, y), px(16.), first_style), + (4..5, point(start + px(24.), y), px(8.), fallback_style), + (5..7, point(start + px(32.), y), px(16.), last_style), + ] + ); + assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original); + + window.next_frame.scene.clear(); + let mut captured = Vec::new(); + line.paint_with_underline_handler( + origin, + line_height, + align, + align_width, + window, + cx, + |range, origin, width, style, _| { + captured.push((range, origin, width, *style)) + }, + ) + .unwrap(); + assert_eq!(captured, strokes); + assert_eq!(window.next_frame.scene.underlines.len(), 0); + } + } + for text in ["", "abc"] { + let line = underline_test_line(text, &[], false, window); + let mut calls = 0; + line.paint_with_underline_handler( + point(px(4.), px(10.)), + px(20.), + TextAlign::Left, + None, + window, + cx, + |_, _, _, _, _| calls += 1, + ) + .unwrap(); + assert_eq!(calls, 0); + } + }); + } + + #[gpui::test] + fn test_underline_handler_reports_zero_advance_geometry(cx: &mut TestAppContext) { + test_underline_handler_at_scales(cx, |window, cx| { + let first_style = UnderlineStyle { + thickness: px(1.), + color: Some(black()), + wavy: true, + }; + let last_style = UnderlineStyle { + wavy: false, + ..first_style + }; + let decoration = DecorationRun { + len: 1, + color: black(), + background_color: None, + underline: Some(first_style), + strikethrough: None, + }; + let line = underline_test_line( + "ab", + &[ + decoration.clone(), + DecorationRun { + underline: Some(last_style), + ..decoration + }, + ], + true, + window, + ); + let half_width = cx + .text_system() + .bounding_box(line.runs[0].font_id, line.font_size) + .size + .width + / 2.; + let origin = point(px(40.25), px(10.25)); + let line_height = px(20.); + let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent); + for (align, offset) in [ + (TextAlign::Left, 0.), + (TextAlign::Center, 16.), + (TextAlign::Right, 32.), + ] { + window.next_frame.scene.clear(); + line.paint(origin, line_height, align, Some(px(32.)), window, cx) + .unwrap(); + let original = window.next_frame.scene.underlines.clone(); + window.next_frame.scene.clear(); + let mut strokes = Vec::new(); + line.paint_with_underline_handler( + origin, + line_height, + align, + Some(px(32.)), + window, + cx, + |range, origin, width, style, window| { + strokes.push((range, origin, width, *style)); + window.paint_underline(origin, width, style); + }, + ) + .unwrap(); + let end = origin.x + px(offset); + let start = point(end - half_width, y); + let width = end - start.x; + assert_eq!( + strokes, + [ + (0..1, start, width, first_style), + (1..2, start, width, last_style), + ] + ); + assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original); + } + }); + } + + #[gpui::test] + fn test_underline_handler_matches_wrapped_paint(cx: &mut TestAppContext) { + test_underline_handler_at_scales(cx, |window, cx| { + let style = UnderlineStyle { + thickness: px(1.), + color: Some(black()), + wavy: true, + }; + for zero_advance in [false, true] { + let line = underline_test_line( + "abcd", + &[DecorationRun { + len: 4, + color: black(), + background_color: None, + underline: Some(style), + strikethrough: None, + }], + zero_advance, + window, + ); + let half_width = cx + .text_system() + .bounding_box(line.runs[0].font_id, line.font_size) + .size + .width + / 2.; + let origin = point(px(40.25), px(10.25)); + let line_height = px(20.); + let y = origin.y + underline_y_offset(line_height, line.ascent, line.descent); + let wrapped = WrappedLine { + layout: Arc::new(WrappedLineLayout { + unwrapped_layout: line.layout, + wrap_boundaries: SmallVec::from_buf([WrapBoundary { + run_ix: 0, + glyph_ix: 2, + }]), + wrap_width: Some(px(16.)), + }), + text: line.text, + decoration_runs: line.decoration_runs.into_vec(), + }; + window.next_frame.scene.clear(); + wrapped + .paint(origin, line_height, TextAlign::Left, None, window, cx) + .unwrap(); + let original = window.next_frame.scene.underlines.clone(); + window.next_frame.scene.clear(); + let mut strokes = Vec::new(); + paint_line( + origin, + &wrapped.unwrapped_layout, + line_height, + TextAlign::Left, + Some(px(16.)), + &wrapped.decoration_runs, + &wrapped.wrap_boundaries, + window, + cx, + &mut |range, origin, width, style, window| { + strokes.push((range, origin, width, *style)); + window.paint_underline(origin, width, style); + }, + ) + .unwrap(); + let (start, width) = if zero_advance { + let start = origin.x - half_width; + (start, origin.x - start) + } else { + (origin.x, px(16.)) + }; + assert_eq!( + strokes, + [ + (0..4, point(start, y), width, style), + (0..4, point(start, y + line_height), width, style), + ] + ); + assert_underline_primitives_eq(&window.next_frame.scene.underlines, &original); + } + }); + } + + #[test] + fn test_split_at_invariants() { + // Split "abcdef" at every possible byte index and verify structural invariants. + let line = make_shaped_line( + "abcdef", + &[ + (0, 0.0), + (1, 10.0), + (2, 20.0), + (3, 30.0), + (4, 40.0), + (5, 50.0), + ], + 60.0, + &[], + ); + + for i in 0..=6 { + let (left, right) = line.split_at(i); + + assert_eq!( + left.width() + right.width(), + line.width(), + "widths must sum at split={i}" + ); + assert_eq!( + left.len() + right.len(), + line.len(), + "lengths must sum at split={i}" + ); + assert_eq!( + format!("{}{}", left.text.as_ref(), right.text.as_ref()), + "abcdef", + "text must concatenate at split={i}" + ); + assert_eq!(left.font_size, line.font_size, "font_size at split={i}"); + assert_eq!(right.ascent, line.ascent, "ascent at split={i}"); + assert_eq!(right.descent, line.descent, "descent at split={i}"); + } + + // Edge: split at 0 produces no left runs, full content on right + let (left, right) = line.split_at(0); + assert_eq!(left.runs.len(), 0); + assert_eq!(right.runs[0].glyphs.len(), 6); + + // Edge: split at end produces full content on left, no right runs + let (left, right) = line.split_at(6); + assert_eq!(left.runs[0].glyphs.len(), 6); + assert_eq!(right.runs.len(), 0); + } + + #[test] + fn test_split_at_glyph_rebasing() { + // Two font runs (simulating a font fallback boundary at byte 3): + // run A (FontId 0): glyphs at bytes 0,1,2 positions 0,10,20 + // run B (FontId 1): glyphs at bytes 3,4,5 positions 30,40,50 + // Successive splits simulate the incremental splitting done during wrap. + let line = ShapedLine { + layout: Arc::new(LineLayout { + font_size: px(16.0), + width: px(60.0), + ascent: px(12.0), + descent: px(4.0), + runs: vec![ + ShapedRun { + font_id: FontId(0), + glyphs: vec![ + ShapedGlyph { + id: GlyphId(0), + position: point(px(0.0), px(0.0)), + index: 0, + is_emoji: false, + }, + ShapedGlyph { + id: GlyphId(0), + position: point(px(10.0), px(0.0)), + index: 1, + is_emoji: false, + }, + ShapedGlyph { + id: GlyphId(0), + position: point(px(20.0), px(0.0)), + index: 2, + is_emoji: false, + }, + ], + }, + ShapedRun { + font_id: FontId(1), + glyphs: vec![ + ShapedGlyph { + id: GlyphId(0), + position: point(px(30.0), px(0.0)), + index: 3, + is_emoji: false, + }, + ShapedGlyph { + id: GlyphId(0), + position: point(px(40.0), px(0.0)), + index: 4, + is_emoji: false, + }, + ShapedGlyph { + id: GlyphId(0), + position: point(px(50.0), px(0.0)), + index: 5, + is_emoji: false, + }, + ], + }, + ], + len: 6, + }), + text: "abcdef".into(), + decoration_runs: SmallVec::new(), + }; + + // First split at byte 2 — mid-run in run A + let (first, remainder) = line.split_at(2); + assert_eq!(first.text.as_ref(), "ab"); + assert_eq!(first.runs.len(), 1); + assert_eq!(first.runs[0].font_id, FontId(0)); + + // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs) + assert_eq!(remainder.text.as_ref(), "cdef"); + assert_eq!(remainder.runs.len(), 2); + assert_eq!(remainder.runs[0].font_id, FontId(0)); + assert_eq!(remainder.runs[0].glyphs.len(), 1); + assert_eq!(remainder.runs[0].glyphs[0].index, 0); + assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0)); + assert_eq!(remainder.runs[1].font_id, FontId(1)); + assert_eq!(remainder.runs[1].glyphs[0].index, 1); + assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0)); + + // Second split at byte 2 within remainder — crosses the run boundary + let (second, final_part) = remainder.split_at(2); + assert_eq!(second.text.as_ref(), "cd"); + assert_eq!(final_part.text.as_ref(), "ef"); + assert_eq!(final_part.runs[0].glyphs[0].index, 0); + assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0)); + + // Widths must sum across all three pieces + assert_eq!( + first.width() + second.width() + final_part.width(), + line.width() + ); + } + + #[test] + fn test_split_at_decorations() { + // Three decoration runs: red [0..2), green [2..5), blue [5..6). + // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right. + let red = Hsla { + h: 0.0, + s: 1.0, + l: 0.5, + a: 1.0, + }; + let green = Hsla { + h: 0.3, + s: 1.0, + l: 0.5, + a: 1.0, + }; + let blue = Hsla { + h: 0.6, + s: 1.0, + l: 0.5, + a: 1.0, + }; + + let line = make_shaped_line( + "abcdef", + &[ + (0, 0.0), + (1, 10.0), + (2, 20.0), + (3, 30.0), + (4, 40.0), + (5, 50.0), + ], + 60.0, + &[ + DecorationRun { + len: 2, + color: red, + background_color: None, + underline: None, + strikethrough: None, + }, + DecorationRun { + len: 3, + color: green, + background_color: None, + underline: None, + strikethrough: None, + }, + DecorationRun { + len: 1, + color: blue, + background_color: None, + underline: None, + strikethrough: None, + }, + ], + ); + + let (left, right) = line.split_at(3); + + // Left: red(2) + green(1) — green straddled, left portion has len 1 + assert_eq!(left.decoration_runs.len(), 2); + assert_eq!(left.decoration_runs[0].len, 2); + assert_eq!(left.decoration_runs[0].color, red); + assert_eq!(left.decoration_runs[1].len, 1); + assert_eq!(left.decoration_runs[1].color, green); + + // Right: green(2) + blue(1) — green straddled, right portion has len 2 + assert_eq!(right.decoration_runs.len(), 2); + assert_eq!(right.decoration_runs[0].len, 2); + assert_eq!(right.decoration_runs[0].color, green); + assert_eq!(right.decoration_runs[1].len, 1); + assert_eq!(right.decoration_runs[1].color, blue); + } + + struct UnderlineHandlerTestView(Rc); + + impl Render for UnderlineHandlerTestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let paint = self.0.clone(); + canvas( + |_, _, _| {}, + move |_, _, window, cx| { + window.with_element_opacity(Some(0.5), |window| paint(window, cx)); + }, + ) + .size_full() + } + } + + fn test_underline_handler_at_scales( + cx: &mut TestAppContext, + paint: impl Fn(&mut Window, &mut App) + 'static, + ) { + let window = cx.add_window(move |_, _| UnderlineHandlerTestView(Rc::new(paint))); + for scale in [1., 1.25, 1.5, 2., 3.] { + cx.simulate_window_scale_factor_change(window.into(), scale); + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + } + } + + fn underline_test_line( + text: &str, + decorations: &[DecorationRun], + zero_advance: bool, + window: &Window, + ) -> ShapedLine { + let mut line = window.text_system().shape_line( + SharedString::new(text), + px(16.), + &[TextRun { + len: text.len(), + font: font(".ZedMono"), + color: black(), + ..TextRun::default() + }], + None, + ); + line.decoration_runs = SmallVec::from(decorations.to_vec()); + let layout = &line.layout; + let mut runs = layout.runs.clone(); + let advance = if zero_advance { px(0.) } else { px(8.) }; + let mut width = px(0.); + for glyph in runs.iter_mut().flat_map(|run| &mut run.glyphs) { + glyph.position.x = width; + width += advance; + } + line.layout = Arc::new(LineLayout { + font_size: layout.font_size, + width, + ascent: layout.ascent, + descent: layout.descent, + runs, + len: layout.len, + }); + line + } + + fn assert_underline_primitives_eq(actual: &[Underline], expected: &[Underline]) { + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected) { + assert_eq!(actual.bounds, expected.bounds); + assert_eq!(actual.content_mask, expected.content_mask); + assert_eq!(actual.color, expected.color); + assert_eq!(actual.thickness, expected.thickness); + assert_eq!(actual.wavy, expected.wavy); + assert_eq!(actual.order, expected.order); + assert_eq!(actual.pad, expected.pad); + } + } + + #[test] + fn test_cursor_preserves_shaping_metadata_across_runs() { + let line = ShapedLine { + layout: Arc::new(LineLayout { + font_size: px(16.0), + width: px(50.0), + ascent: px(12.0), + descent: px(4.0), + runs: vec![ + ShapedRun { + font_id: FontId(3), + glyphs: vec![ + ShapedGlyph { + id: GlyphId(11), + position: point(px(0.0), px(1.0)), + index: 0, + is_emoji: true, + }, + ShapedGlyph { + id: GlyphId(12), + position: point(px(17.0), px(1.0)), + index: 1, + is_emoji: false, + }, + ShapedGlyph { + id: GlyphId(13), + position: point(px(19.0), px(-1.0)), + index: 1, + is_emoji: false, + }, + ], + }, + ShapedRun { + font_id: FontId(8), + glyphs: vec![ + ShapedGlyph { + id: GlyphId(21), + position: point(px(25.0), px(1.0)), + index: 5, + is_emoji: true, + }, + ShapedGlyph { + id: GlyphId(22), + position: point(px(41.0), px(1.0)), + index: 7, + is_emoji: false, + }, + ], + }, + ], + len: 10, + }), + text: "a😀bcdef".into(), + decoration_runs: SmallVec::new(), + }; + let mut cursor = line.cursor(); + let first = cursor.take_until(5); + assert_eq!(first.text.as_ref(), "a😀"); + assert_eq!(first.runs[0].font_id, FontId(3)); + assert_eq!(first.runs[0].glyphs[0].id, GlyphId(11)); + assert!(first.runs[0].glyphs[0].is_emoji); + assert_eq!(first.runs[0].glyphs[1].index, 1); + assert_eq!(first.runs[0].glyphs[1].position, point(px(17.0), px(1.0))); + assert_eq!(first.runs[0].glyphs.len(), 3); + assert_eq!(first.runs[0].glyphs[2].index, 1); + assert_eq!(first.runs[0].glyphs[2].position, point(px(19.0), px(-1.0))); + assert_eq!(cursor.x_offset(), px(25.0)); + + let second = cursor.take_until(7); + assert_eq!(second.text.as_ref(), "bc"); + assert_eq!(second.runs[0].font_id, FontId(8)); + assert_eq!(second.runs[0].glyphs[0].id, GlyphId(21)); + assert_eq!(second.runs[0].glyphs[0].index, 0); + assert_eq!(second.runs[0].glyphs[0].position, point(px(0.0), px(1.0))); + assert_eq!(cursor.x_offset(), px(41.0)); + + let final_part = cursor.take_until(10); + assert_eq!(final_part.text.as_ref(), "def"); + assert_eq!(final_part.runs[0].font_id, FontId(8)); + assert_eq!(final_part.runs[0].glyphs[0].id, GlyphId(22)); + assert_eq!(final_part.runs[0].glyphs[0].index, 0); + assert_eq!( + final_part.runs[0].glyphs[0].position, + point(px(0.0), px(1.0)) + ); + } + + #[test] + fn test_cursor_preserves_existing_visual_order_splitting() { + let line = make_shaped_line("abc", &[(0, 0.0), (2, 10.0), (1, 20.0)], 30.0, &[]); + let mut cursor = line.cursor(); + let mut remainder = line.clone(); + let mut previous_boundary = 0; + for boundary in [0, 1, 2, 3] { + let (expected, rest) = remainder.split_at(boundary - previous_boundary); + let actual = cursor.take_until(boundary); + assert_eq!(actual.text, expected.text); + assert_eq!(actual.width(), expected.width()); + assert_eq!(actual.runs.len(), expected.runs.len()); + for (actual, expected) in actual.runs.iter().zip(&expected.runs) { + assert_eq!(actual.font_id, expected.font_id); + assert_eq!(actual.glyphs.len(), expected.glyphs.len()); + for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) { + assert_eq!(actual.id, expected.id); + assert_eq!(actual.index, expected.index); + assert_eq!(actual.position, expected.position); + } + } + assert_eq!(cursor.x_offset(), line.x_for_index(boundary)); + remainder = rest; + previous_boundary = boundary; + } + } + + #[test] + fn test_cursor_partitions_one_decoration_across_three_chunks() { + let line = make_shaped_line( + "abcdef", + &[ + (0, 0.0), + (1, 10.0), + (2, 20.0), + (3, 30.0), + (4, 40.0), + (5, 50.0), + ], + 60.0, + &[DecorationRun { + len: 6, + color: Hsla { + h: 0.2, + s: 0.4, + l: 0.6, + a: 1.0, + }, + background_color: None, + underline: None, + strikethrough: None, + }], + ); + let mut cursor = line.cursor(); + assert_eq!(cursor.take_until(2).decoration_runs[0].len, 2); + assert_eq!(cursor.take_until(4).decoration_runs[0].len, 2); + assert_eq!(cursor.take_until(6).decoration_runs[0].len, 2); + } + + #[test] + fn test_cursor_matches_successive_splits_at_ordered_boundaries() { + let decorations: Vec<_> = [2, 0, 3, 1] + .into_iter() + .map(|len| DecorationRun { + len, + color: Hsla { + h: len as f32 / 10.0, + s: 0.5, + l: 0.5, + a: 1.0, + }, + background_color: Some(black()), + underline: None, + strikethrough: None, + }) + .collect(); + let line = make_shaped_line( + "abcdef", + &[(0, 5.0), (0, 5.0), (2, 15.0), (4, 25.0), (5, 35.0)], + 45.0, + &decorations, + ); + for first in 0..=line.len() { + for second in first..=line.len() { + let mut cursor = line.cursor(); + let mut remainder = line.clone(); + let mut previous_boundary = 0; + let mut total_width = px(0.0); + let mut text = String::new(); + for boundary in [first, second, line.len(), line.len()] { + let (expected, rest) = remainder.split_at(boundary - previous_boundary); + let actual = cursor.take_until(boundary); + assert_eq!(actual.text, expected.text); + assert_eq!(actual.len(), expected.len()); + assert_eq!(actual.width(), expected.width()); + assert_eq!(actual.runs.len(), expected.runs.len()); + for (actual, expected) in actual.runs.iter().zip(&expected.runs) { + assert_eq!(actual.font_id, expected.font_id); + assert_eq!(actual.glyphs.len(), expected.glyphs.len()); + for (actual, expected) in actual.glyphs.iter().zip(&expected.glyphs) { + assert_eq!(actual.id, expected.id); + assert_eq!(actual.index, expected.index); + assert_eq!(actual.position, expected.position); + } + } + assert_eq!(actual.decoration_runs.len(), expected.decoration_runs.len()); + for (actual, expected) in + actual.decoration_runs.iter().zip(&expected.decoration_runs) + { + assert_eq!(actual.len, expected.len); + assert_eq!(actual.color, expected.color); + assert_eq!(actual.background_color, expected.background_color); + } + total_width += actual.width(); + text.push_str(&actual.text); + remainder = rest; + previous_boundary = boundary; + } + assert_eq!(total_width, line.width()); + assert_eq!(text, line.text.as_ref()); + } + } + } + + #[test] + fn test_cursor_empty_chunks_and_repeated_boundaries() { + let line = make_shaped_line("ab", &[(0, 5.0), (1, 15.0)], 20.0, &[]); + let mut cursor = line.cursor(); + assert_eq!(cursor.take_until(0).text.as_ref(), ""); + assert_eq!(cursor.take_until(0).text.as_ref(), ""); + assert_eq!(cursor.take_until(1).text.as_ref(), "a"); + assert_eq!(cursor.take_until(2).text.as_ref(), "b"); + assert_eq!(cursor.take_until(2).text.as_ref(), ""); + let empty = make_shaped_line("", &[], 0.0, &[]); + let piece = empty.cursor().take_until(0); + assert!(piece.text.is_empty()); + assert!(piece.runs.is_empty()); + assert_eq!(piece.width(), px(0.0)); + } + + #[test] + fn test_cursor_rejects_invalid_boundaries() { + let line = make_shaped_line("é", &[(0, 0.0)], 10.0, &[]); + let mut cursor = line.cursor(); + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + cursor.take_until(1); + })) + .is_err() + ); + let mut cursor = line.cursor(); + cursor.take_until(2); + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + cursor.take_until(0); + })) + .is_err() + ); + let mut cursor = line.cursor(); + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + cursor.take_until(3); + })) + .is_err() + ); + } +} diff --git a/vendor/gpui-pre/src/text_system/line_layout.rs b/vendor/gpui-pre/src/text_system/line_layout.rs new file mode 100644 index 0000000..f0bf121 --- /dev/null +++ b/vendor/gpui-pre/src/text_system/line_layout.rs @@ -0,0 +1,1190 @@ +use crate::{FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size, point, px}; +use collections::FxHashMap; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + hash::{Hash, Hasher}, + ops::Range, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +use super::LineWrapper; + +/// A laid out and styled line of text +#[derive(Default, Debug)] +pub struct LineLayout { + /// The font size for this line + pub font_size: Pixels, + /// The width of the line + pub width: Pixels, + /// The ascent of the line + pub ascent: Pixels, + /// The descent of the line + pub descent: Pixels, + /// The shaped runs that make up this line + pub runs: Vec, + /// The length of the line in utf-8 bytes + pub len: usize, +} + +/// A run of text that has been shaped . +#[derive(Debug, Clone)] +pub struct ShapedRun { + /// The font id for this run + pub font_id: FontId, + /// The glyphs that make up this run + pub glyphs: Vec, +} + +/// A single glyph, ready to paint. +#[derive(Clone, Debug)] +pub struct ShapedGlyph { + /// The ID for this glyph, as determined by the text system. + pub id: GlyphId, + + /// The position of this glyph in its containing line. + pub position: Point, + + /// The index of this glyph in the original text. + pub index: usize, + + /// Whether this glyph is an emoji + pub is_emoji: bool, +} + +impl LineLayout { + /// The index for the character at the given x coordinate + pub fn index_for_x(&self, x: Pixels) -> Option { + if x >= self.width { + None + } else { + for run in self.runs.iter().rev() { + for glyph in run.glyphs.iter().rev() { + if glyph.position.x <= x { + return Some(glyph.index); + } + } + } + Some(0) + } + } + + /// closest_index_for_x returns the character boundary closest to the given x coordinate + /// (e.g. to handle aligning up/down arrow keys) + pub fn closest_index_for_x(&self, x: Pixels) -> usize { + let mut prev_index = 0; + let mut prev_x = px(0.); + + for run in self.runs.iter() { + for glyph in run.glyphs.iter() { + if glyph.position.x >= x { + if glyph.position.x - x < x - prev_x { + return glyph.index; + } else { + return prev_index; + } + } + prev_index = glyph.index; + prev_x = glyph.position.x; + } + } + + if self.len == 1 { + if x > self.width / 2. { + return 1; + } else { + return 0; + } + } + + self.len + } + + /// The x position of the character at the given index + pub fn x_for_index(&self, index: usize) -> Pixels { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return glyph.position.x; + } + } + } + self.width + } + + /// The corresponding Font at the given index + pub fn font_id_for_index(&self, index: usize) -> Option { + for run in &self.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return Some(run.font_id); + } + } + } + None + } + + /// Split this layout at a byte index, returning `(prefix, suffix)`. + /// + /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions. + /// Its width equals the x-advance up to the split point. + /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions + /// shifted left so the first glyph starts at x=0, and byte indices rebased to 0. + /// - `font_size`, `ascent`, and `descent` are copied to both halves. + pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) { + let x_offset = self.x_for_index(byte_index); + + // Partition glyph runs. A single run may contribute glyphs to both halves. + let mut left_runs = Vec::new(); + let mut right_runs = Vec::new(); + + for run in &self.runs { + let split_pos = run.glyphs.partition_point(|g| g.index < byte_index); + + if split_pos > 0 { + left_runs.push(ShapedRun { + font_id: run.font_id, + glyphs: run.glyphs[..split_pos].to_vec(), + }); + } + + if split_pos < run.glyphs.len() { + let right_glyphs = run.glyphs[split_pos..] + .iter() + .map(|g| ShapedGlyph { + id: g.id, + position: point(g.position.x - x_offset, g.position.y), + index: g.index - byte_index, + is_emoji: g.is_emoji, + }) + .collect(); + right_runs.push(ShapedRun { + font_id: run.font_id, + glyphs: right_glyphs, + }); + } + } + + let left = LineLayout { + font_size: self.font_size, + width: x_offset, + ascent: self.ascent, + descent: self.descent, + runs: left_runs, + len: byte_index, + }; + + let right = LineLayout { + font_size: self.font_size, + width: self.width - x_offset, + ascent: self.ascent, + descent: self.descent, + runs: right_runs, + len: self.len - byte_index, + }; + + (left, right) + } + + fn compute_wrap_boundaries( + &self, + text: &str, + wrap_width: Pixels, + max_lines: Option, + ) -> SmallVec<[WrapBoundary; 1]> { + let mut boundaries = SmallVec::new(); + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = None; + let mut last_candidate_x = px(0.); + let mut last_boundary = WrapBoundary { + run_ix: 0, + glyph_ix: 0, + }; + let mut last_boundary_x = px(0.); + let mut prev_ch = '\0'; + let mut glyphs = self + .runs + .iter() + .enumerate() + .flat_map(move |(run_ix, run)| { + run.glyphs.iter().enumerate().map(move |(glyph_ix, glyph)| { + let character = text[glyph.index..].chars().next().unwrap(); + ( + WrapBoundary { run_ix, glyph_ix }, + character, + glyph.position.x, + ) + }) + }) + .peekable(); + + while let Some((boundary, ch, x)) = glyphs.next() { + if ch == '\n' { + continue; + } + + // Here is very similar to `LineWrapper::wrap_line` to determine text wrapping, + // but there are some differences, so we have to duplicate the code here. + if LineWrapper::is_word_char(ch) { + if prev_ch == ' ' && ch != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(boundary); + last_candidate_x = x; + } + } else { + if ch != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = Some(boundary); + last_candidate_x = x; + } + } + + if ch != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(boundary); + } + + let next_x = glyphs.peek().map_or(self.width, |(_, _, x)| *x); + let width = next_x - last_boundary_x; + + if width > wrap_width && boundary > last_boundary { + // When used line_clamp, we should limit the number of lines. + if let Some(max_lines) = max_lines + && boundaries.len() >= max_lines.saturating_sub(1) + { + break; + } + + if let Some(last_candidate_ix) = last_candidate_ix.take() { + last_boundary = last_candidate_ix; + last_boundary_x = last_candidate_x; + } else { + last_boundary = boundary; + last_boundary_x = x; + } + boundaries.push(last_boundary); + } + prev_ch = ch; + } + + boundaries + } +} + +/// A line of text that has been wrapped to fit a given width +#[derive(Default, Debug)] +pub struct WrappedLineLayout { + /// The line layout, pre-wrapping. + pub unwrapped_layout: Arc, + + /// The boundaries at which the line was wrapped + pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + + /// The width of the line, if it was wrapped + pub wrap_width: Option, +} + +/// A boundary at which a line was wrapped +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct WrapBoundary { + /// The index in the run just before the line was wrapped + pub run_ix: usize, + /// The index of the glyph just before the line was wrapped + pub glyph_ix: usize, +} + +impl WrappedLineLayout { + /// The length of the underlying text, in utf8 bytes. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.unwrapped_layout.len + } + + /// The width of this line, in pixels, whether or not it was wrapped. + pub fn width(&self) -> Pixels { + self.wrap_width + .unwrap_or(Pixels::MAX) + .min(self.unwrapped_layout.width) + } + + /// The size of the whole wrapped text, for the given line_height. + /// can span multiple lines if there are multiple wrap boundaries. + pub fn size(&self, line_height: Pixels) -> Size { + Size { + width: self.width(), + height: line_height * (self.wrap_boundaries.len() + 1), + } + } + + /// The ascent of a line in this layout + pub fn ascent(&self) -> Pixels { + self.unwrapped_layout.ascent + } + + /// The descent of a line in this layout + pub fn descent(&self) -> Pixels { + self.unwrapped_layout.descent + } + + /// The wrap boundaries in this layout + pub fn wrap_boundaries(&self) -> &[WrapBoundary] { + &self.wrap_boundaries + } + + /// The font size of this layout + pub fn font_size(&self) -> Pixels { + self.unwrapped_layout.font_size + } + + /// The runs in this layout, sans wrapping + pub fn runs(&self) -> &[ShapedRun] { + &self.unwrapped_layout.runs + } + + /// The index corresponding to a given position in this layout for the given line height. + /// + /// See also [`Self::closest_index_for_position`]. + pub fn index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Result { + self._index_for_position(position, line_height, false) + } + + /// The closest index to a given position in this layout for the given line height. + /// + /// Closest means the character boundary closest to the given position. + /// + /// See also [`LineLayout::closest_index_for_x`]. + pub fn closest_index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Result { + self._index_for_position(position, line_height, true) + } + + fn _index_for_position( + &self, + mut position: Point, + line_height: Pixels, + closest: bool, + ) -> Result { + let wrapped_line_ix = (position.y / line_height) as usize; + + let wrapped_line_start_index; + let wrapped_line_start_x; + if wrapped_line_ix > 0 { + let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else { + return Err(0); + }; + let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix]; + let glyph = &run.glyphs[line_start_boundary.glyph_ix]; + wrapped_line_start_index = glyph.index; + wrapped_line_start_x = glyph.position.x; + } else { + wrapped_line_start_index = 0; + wrapped_line_start_x = Pixels::ZERO; + }; + + let wrapped_line_end_index; + let wrapped_line_end_x; + if wrapped_line_ix < self.wrap_boundaries.len() { + let next_wrap_boundary_ix = wrapped_line_ix; + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; + let glyph = &run.glyphs[next_wrap_boundary.glyph_ix]; + wrapped_line_end_index = glyph.index; + wrapped_line_end_x = glyph.position.x; + } else { + wrapped_line_end_index = self.unwrapped_layout.len; + wrapped_line_end_x = self.unwrapped_layout.width; + }; + + let mut position_in_unwrapped_line = position; + position_in_unwrapped_line.x += wrapped_line_start_x; + if position_in_unwrapped_line.x < wrapped_line_start_x { + Err(wrapped_line_start_index) + } else if position_in_unwrapped_line.x >= wrapped_line_end_x { + Err(wrapped_line_end_index) + } else { + if closest { + Ok(self + .unwrapped_layout + .closest_index_for_x(position_in_unwrapped_line.x)) + } else { + // The shaper can place a trailing zero-width wrap boundary glyph slightly past + // the line's width, so the row can extend past where `index_for_x` has glyphs. + self.unwrapped_layout + .index_for_x(position_in_unwrapped_line.x) + .ok_or(wrapped_line_end_index) + } + } + } + + /// Returns the pixel position for the given byte index. + pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option> { + let mut line_start_ix = 0; + let mut line_end_indices = self + .wrap_boundaries + .iter() + .map(|wrap_boundary| { + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; + let glyph = &run.glyphs[wrap_boundary.glyph_ix]; + glyph.index + }) + .chain([self.len()]) + .enumerate(); + for (ix, line_end_ix) in line_end_indices { + let line_y = ix as f32 * line_height; + if index < line_start_ix { + break; + } else if index > line_end_ix { + line_start_ix = line_end_ix; + continue; + } else { + let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix); + let x = self.unwrapped_layout.x_for_index(index) - line_start_x; + return Some(point(x, line_y)); + } + } + + None + } +} + +pub(crate) struct LineLayoutCache { + previous_frame: Mutex, + current_frame: RwLock, + platform_text_system: Arc, + /// Advances when [`TextSystem::add_fonts`] successfully changes the font database. + font_generation: Arc, + /// Records the generation represented by both frame caches. + cached_font_generation: AtomicUsize, +} + +#[derive(Default)] +struct FrameCache { + lines: FxHashMap, Arc>, + wrapped_lines: FxHashMap, Arc>, + used_lines: Vec>, + used_wrapped_lines: Vec>, + + // Content-addressable caches keyed by caller-provided text hash + layout params. + // These allow cache hits without materializing a contiguous `SharedString`. + // + // IMPORTANT: To support allocation-free lookups, we store these maps using a key type + // (`HashedCacheKeyRef`) that can be computed without building a contiguous `&str`/`SharedString`. + // On miss, we allocate once and store under an owned `HashedCacheKey`. + lines_by_hash: FxHashMap, Arc>, + wrapped_lines_by_hash: FxHashMap, Arc>, + used_lines_by_hash: Vec>, + used_wrapped_lines_by_hash: Vec>, +} + +#[derive(Clone, Default)] +pub(crate) struct LineLayoutIndex { + font_generation: usize, + lines_index: usize, + wrapped_lines_index: usize, + lines_by_hash_index: usize, + wrapped_lines_by_hash_index: usize, +} + +impl LineLayoutCache { + pub fn new( + platform_text_system: Arc, + font_generation: Arc, + ) -> Self { + let cached_font_generation = font_generation.load(Ordering::Acquire); + Self { + previous_frame: Mutex::default(), + current_frame: RwLock::default(), + platform_text_system, + font_generation, + cached_font_generation: AtomicUsize::new(cached_font_generation), + } + } + + pub fn layout_index(&self) -> LineLayoutIndex { + let font_generation = self.clear_if_font_generation_changed(); + let frame = self.current_frame.read(); + LineLayoutIndex { + font_generation, + lines_index: frame.used_lines.len(), + wrapped_lines_index: frame.used_wrapped_lines.len(), + lines_by_hash_index: frame.used_lines_by_hash.len(), + wrapped_lines_by_hash_index: frame.used_wrapped_lines_by_hash.len(), + } + } + + pub fn reuse_layouts(&self, range: Range) { + let font_generation = self.clear_if_font_generation_changed(); + if range.start.font_generation != font_generation + || range.end.font_generation != font_generation + { + return; + } + let mut current_frame = &mut *self.current_frame.write(); + let mut previous_frame = &mut *self.previous_frame.lock(); + + for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); + } + + for key in &previous_frame.used_wrapped_lines + [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); + } + + for key in &previous_frame.used_lines_by_hash + [range.start.lines_by_hash_index..range.end.lines_by_hash_index] + { + if let Some((key, line)) = previous_frame.lines_by_hash.remove_entry(key) { + current_frame.lines_by_hash.insert(key, line); + } + current_frame.used_lines_by_hash.push(key.clone()); + } + + for key in &previous_frame.used_wrapped_lines_by_hash + [range.start.wrapped_lines_by_hash_index..range.end.wrapped_lines_by_hash_index] + { + if let Some((key, line)) = previous_frame.wrapped_lines_by_hash.remove_entry(key) { + current_frame.wrapped_lines_by_hash.insert(key, line); + } + current_frame.used_wrapped_lines_by_hash.push(key.clone()); + } + } + + pub fn truncate_layouts(&self, index: LineLayoutIndex) { + let font_generation = self.clear_if_font_generation_changed(); + if index.font_generation != font_generation { + return; + } + let mut current_frame = &mut *self.current_frame.write(); + current_frame.used_lines.truncate(index.lines_index); + current_frame + .used_wrapped_lines + .truncate(index.wrapped_lines_index); + current_frame + .used_lines_by_hash + .truncate(index.lines_by_hash_index); + current_frame + .used_wrapped_lines_by_hash + .truncate(index.wrapped_lines_by_hash_index); + } + + pub fn finish_frame(&self) { + let _font_generation = self.clear_if_font_generation_changed(); + let mut curr_frame = self.current_frame.write(); + let mut prev_frame = self.previous_frame.lock(); + std::mem::swap(&mut *prev_frame, &mut *curr_frame); + curr_frame.lines.clear(); + curr_frame.wrapped_lines.clear(); + curr_frame.used_lines.clear(); + curr_frame.used_wrapped_lines.clear(); + + curr_frame.lines_by_hash.clear(); + curr_frame.wrapped_lines_by_hash.clear(); + curr_frame.used_lines_by_hash.clear(); + curr_frame.used_wrapped_lines_by_hash.clear(); + } + + pub fn layout_wrapped_line( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], + wrap_width: Option, + max_lines: Option, + ) -> Arc + where + Text: AsRef, + SharedString: From, + { + let _font_generation = self.clear_if_font_generation_changed(); + let key = &CacheKeyRef { + text: text.as_ref(), + font_size, + runs, + wrap_width, + force_width: None, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.wrapped_lines.get(key) { + return layout.clone(); + } + + let previous_frame_entry = self.previous_frame.lock().wrapped_lines.remove_entry(key); + if let Some((key, layout)) = previous_frame_entry { + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + layout + } else { + drop(current_frame); + let text = SharedString::from(text); + let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None); + let wrap_boundaries = if let Some(wrap_width) = wrap_width { + unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines) + } else { + SmallVec::new() + }; + let layout = Arc::new(WrappedLineLayout { + unwrapped_layout, + wrap_boundaries, + wrap_width, + }); + let key = Arc::new(CacheKey { + text, + font_size, + runs: SmallVec::from(runs), + wrap_width, + force_width: None, + }); + + let mut current_frame = self.current_frame.write(); + current_frame + .wrapped_lines + .insert(key.clone(), layout.clone()); + current_frame.used_wrapped_lines.push(key); + + layout + } + } + + pub fn layout_line( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], + force_width: Option, + ) -> Arc + where + Text: AsRef, + SharedString: From, + { + let _font_generation = self.clear_if_font_generation_changed(); + let key = &CacheKeyRef { + text: text.as_ref(), + font_size, + runs, + wrap_width: None, + force_width, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.lines.get(key) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) { + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } else { + let text = SharedString::from(text); + let mut layout = self + .platform_text_system + .layout_line(&text, font_size, runs); + + if let Some(force_width) = force_width { + apply_force_width_to_layout(&mut layout, force_width); + } + + let key = Arc::new(CacheKey { + text, + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + force_width, + }); + let layout = Arc::new(layout); + current_frame.lines.insert(key.clone(), layout.clone()); + current_frame.used_lines.push(key); + layout + } + } + + /// Try to retrieve a previously-shaped line layout using a caller-provided content hash. + /// + /// This is a *non-allocating* cache probe: it does not materialize any text. If the layout + /// is not already cached in either the current frame or previous frame, returns `None`. + /// + /// Contract (caller enforced): + /// - Same `text_hash` implies identical text content (collision risk accepted by caller). + /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). + pub fn try_layout_line_by_hash( + &self, + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &[FontRun], + force_width: Option, + ) -> Option> { + let _font_generation = self.clear_if_font_generation_changed(); + let key_ref = HashedCacheKeyRef { + text_hash, + text_len, + font_size, + runs, + wrap_width: None, + force_width, + }; + + let current_frame = self.current_frame.read(); + if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| { + HashedCacheKeyRef { + text_hash: key.text_hash, + text_len: key.text_len, + font_size: key.font_size, + runs: key.runs.as_slice(), + wrap_width: key.wrap_width, + force_width: key.force_width, + } == key_ref + }) { + return Some(layout.clone()); + } + + let previous_frame = self.previous_frame.lock(); + if let Some((_, layout)) = previous_frame.lines_by_hash.iter().find(|(key, _)| { + HashedCacheKeyRef { + text_hash: key.text_hash, + text_len: key.text_len, + font_size: key.font_size, + runs: key.runs.as_slice(), + wrap_width: key.wrap_width, + force_width: key.force_width, + } == key_ref + }) { + return Some(layout.clone()); + } + + None + } + + /// Layout a line of text using a caller-provided content hash as the cache key. + /// + /// This enables cache hits without materializing a contiguous `SharedString` for `text`. + /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping. + /// + /// Contract (caller enforced): + /// - Same `text_hash` implies identical text content (collision risk accepted by caller). + /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions). + pub fn layout_line_by_hash( + &self, + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &[FontRun], + force_width: Option, + materialize_text: impl FnOnce() -> SharedString, + ) -> Arc { + let _font_generation = self.clear_if_font_generation_changed(); + let key_ref = HashedCacheKeyRef { + text_hash, + text_len, + font_size, + runs, + wrap_width: None, + force_width, + }; + + // Fast path: already cached (no allocation). + let current_frame = self.current_frame.upgradable_read(); + if let Some((_, layout)) = current_frame.lines_by_hash.iter().find(|(key, _)| { + HashedCacheKeyRef { + text_hash: key.text_hash, + text_len: key.text_len, + font_size: key.font_size, + runs: key.runs.as_slice(), + wrap_width: key.wrap_width, + force_width: key.force_width, + } == key_ref + }) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + + // Try to reuse from previous frame without allocating; do a linear scan to find a matching key. + // (We avoid `drain()` here because it would eagerly move all entries.) + let mut previous_frame = self.previous_frame.lock(); + if let Some(existing_key) = previous_frame + .used_lines_by_hash + .iter() + .find(|key| { + HashedCacheKeyRef { + text_hash: key.text_hash, + text_len: key.text_len, + font_size: key.font_size, + runs: key.runs.as_slice(), + wrap_width: key.wrap_width, + force_width: key.force_width, + } == key_ref + }) + .cloned() + { + if let Some((key, layout)) = previous_frame.lines_by_hash.remove_entry(&existing_key) { + current_frame + .lines_by_hash + .insert(key.clone(), layout.clone()); + current_frame.used_lines_by_hash.push(key); + return layout; + } + } + + let text = materialize_text(); + let mut layout = self + .platform_text_system + .layout_line(&text, font_size, runs); + + if let Some(force_width) = force_width { + apply_force_width_to_layout(&mut layout, force_width); + } + + let key = Arc::new(HashedCacheKey { + text_hash, + text_len, + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + force_width, + }); + let layout = Arc::new(layout); + current_frame + .lines_by_hash + .insert(key.clone(), layout.clone()); + current_frame.used_lines_by_hash.push(key); + layout + } + + fn clear_if_font_generation_changed(&self) -> usize { + let font_generation = self.font_generation.load(Ordering::Acquire); + if self.cached_font_generation.load(Ordering::Acquire) == font_generation { + return font_generation; + } + + let mut current_frame = self.current_frame.write(); + if self.cached_font_generation.load(Ordering::Acquire) == font_generation { + return font_generation; + } + + *current_frame = FrameCache::default(); + *self.previous_frame.lock() = FrameCache::default(); + self.cached_font_generation + .store(font_generation, Ordering::Release); + font_generation + } +} + +// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by +// HarfBuzz at the same x position as their base character. The force-width +// loop must not advance the cell counter for these zero-advance glyphs, +// otherwise they get displaced into the next cell. We detect them by checking +// whether shaped x has advanced by at least half a cell beyond the last base. +fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) { + let mut glyph_pos: usize = 0; + // NEG_INFINITY ensures the first glyph is always classified as a base. + let mut last_base_shaped_x = px(f32::NEG_INFINITY); + let mut last_base_actual_x = px(0.); + + for run in layout.runs.iter_mut() { + for glyph in run.glyphs.iter_mut() { + let shaped_x = glyph.position.x; + + if shaped_x > last_base_shaped_x + force_width * 0.5 { + let forced_x = glyph_pos * force_width; + if (shaped_x - forced_x).abs() > px(1.) { + glyph.position.x = forced_x; + } + last_base_shaped_x = shaped_x; + last_base_actual_x = glyph.position.x; + glyph_pos += 1; + } else { + glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x); + } + } + } +} + +/// A run of text with a single font. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[expect(missing_docs)] +pub struct FontRun { + pub len: usize, + pub font_id: FontId, +} + +trait AsCacheKeyRef { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_>; +} + +#[derive(Clone, Debug, Eq)] +struct CacheKey { + text: SharedString, + font_size: Pixels, + runs: SmallVec<[FontRun; 1]>, + wrap_width: Option, + force_width: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct CacheKeyRef<'a> { + text: &'a str, + font_size: Pixels, + runs: &'a [FontRun], + wrap_width: Option, + force_width: Option, +} + +#[derive(Clone, Debug)] +struct HashedCacheKey { + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: SmallVec<[FontRun; 1]>, + wrap_width: Option, + force_width: Option, +} + +#[derive(Copy, Clone)] +struct HashedCacheKeyRef<'a> { + text_hash: u64, + text_len: usize, + font_size: Pixels, + runs: &'a [FontRun], + wrap_width: Option, + force_width: Option, +} + +impl PartialEq for dyn AsCacheKeyRef + '_ { + fn eq(&self, other: &dyn AsCacheKeyRef) -> bool { + self.as_cache_key_ref() == other.as_cache_key_ref() + } +} + +impl PartialEq for HashedCacheKey { + fn eq(&self, other: &Self) -> bool { + self.text_hash == other.text_hash + && self.text_len == other.text_len + && self.font_size == other.font_size + && self.runs.as_slice() == other.runs.as_slice() + && self.wrap_width == other.wrap_width + && self.force_width == other.force_width + } +} + +impl Eq for HashedCacheKey {} + +impl Hash for HashedCacheKey { + fn hash(&self, state: &mut H) { + self.text_hash.hash(state); + self.text_len.hash(state); + self.font_size.hash(state); + self.runs.as_slice().hash(state); + self.wrap_width.hash(state); + self.force_width.hash(state); + } +} + +impl PartialEq for HashedCacheKeyRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.text_hash == other.text_hash + && self.text_len == other.text_len + && self.font_size == other.font_size + && self.runs == other.runs + && self.wrap_width == other.wrap_width + && self.force_width == other.force_width + } +} + +impl Eq for HashedCacheKeyRef<'_> {} + +impl Hash for HashedCacheKeyRef<'_> { + fn hash(&self, state: &mut H) { + self.text_hash.hash(state); + self.text_len.hash(state); + self.font_size.hash(state); + self.runs.hash(state); + self.wrap_width.hash(state); + self.force_width.hash(state); + } +} + +impl Eq for dyn AsCacheKeyRef + '_ {} + +impl Hash for dyn AsCacheKeyRef + '_ { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state) + } +} + +impl AsCacheKeyRef for CacheKey { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { + CacheKeyRef { + text: &self.text, + font_size: self.font_size, + runs: self.runs.as_slice(), + wrap_width: self.wrap_width, + force_width: self.force_width, + } + } +} + +impl PartialEq for CacheKey { + fn eq(&self, other: &Self) -> bool { + self.as_cache_key_ref().eq(&other.as_cache_key_ref()) + } +} + +impl Hash for CacheKey { + fn hash(&self, state: &mut H) { + self.as_cache_key_ref().hash(state); + } +} + +impl<'a> Borrow for Arc { + fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) { + self.as_ref() as &dyn AsCacheKeyRef + } +} + +impl AsCacheKeyRef for CacheKeyRef<'_> { + fn as_cache_key_ref(&self) -> CacheKeyRef<'_> { + *self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::GlyphId; + + fn glyph_at(x: f32, index: usize) -> ShapedGlyph { + ShapedGlyph { + id: GlyphId(0), + position: point(px(x), px(0.)), + index, + is_emoji: false, + } + } + + fn make_layout(glyphs: Vec) -> LineLayout { + LineLayout { + font_size: px(16.), + width: px(100.), + ascent: px(12.), + descent: px(4.), + runs: vec![ShapedRun { + font_id: FontId(0), + glyphs, + }], + len: 0, + } + } + + fn glyph_x_positions(layout: &LineLayout) -> Vec { + layout.runs[0] + .glyphs + .iter() + .map(|g| f32::from(g.position.x)) + .collect() + } + + #[test] + fn test_force_width_latin_unchanged() { + let cell_width = px(8.); + let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0., 8., 16.]); + } + + #[test] + fn test_force_width_combining_marks_not_advanced() { + let cell_width = px(8.); + // Simulates Thai "กี" — base consonant at x=0, combining vowel also at x=0 + let mut layout = make_layout(vec![ + glyph_at(0., 0), // ก (base) + glyph_at(0., 3), // ี (combining mark, same x) + ]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0., 0.]); + } + + #[test] + fn test_force_width_base_after_combining_mark() { + let cell_width = px(8.); + let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0., 0., 8.]); + } + + #[test] + fn test_force_width_multiple_combining_marks() { + let cell_width = px(8.); + // Simulates "ก้" — base + vowel + tone mark (two combining marks stacked) + let mut layout = make_layout(vec![ + glyph_at(0., 0), // ก (base) + glyph_at(0., 3), // vowel (combining) + glyph_at(0., 6), // tone mark (combining) + glyph_at(8., 9), // next base + ]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0., 0., 0., 8.]); + } + + #[test] + fn test_force_width_corrects_drifted_base_positions() { + let cell_width = px(8.); + // Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary + let mut layout = make_layout(vec![ + glyph_at(0.5, 0), // within 1px tolerance, kept as-is + glyph_at(10.2, 1), // >1px off from 8.0, corrected + glyph_at(19.8, 2), // >1px off from 16.0, corrected + ]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0.5, 8., 16.]); + } + + #[test] + fn test_force_width_combining_mark_after_within_tolerance_base() { + let cell_width = px(8.); + // Base glyph is within 1px of grid so it keeps its shaped position. + // The combining mark must align to the base's actual position, not the grid slot. + let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]); + + apply_force_width_to_layout(&mut layout, cell_width); + + let positions = glyph_x_positions(&layout); + assert_eq!(positions, vec![0.5, 0.5]); + } +} diff --git a/vendor/gpui-pre/src/text_system/line_wrapper.rs b/vendor/gpui-pre/src/text_system/line_wrapper.rs new file mode 100644 index 0000000..ca821bb --- /dev/null +++ b/vendor/gpui-pre/src/text_system/line_wrapper.rs @@ -0,0 +1,1921 @@ +use crate::{FontId, Pixels, SharedString, TextRun, TextSystem, px}; +use collections::HashMap; +use std::{borrow::Cow, iter, sync::Arc}; + +/// Determines whether to truncate text from the start or end. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TruncateFrom { + /// Truncate text from the start. + Start, + /// Truncate text from the end. + End, + /// Truncate text from the middle, preserving the start and end. + Middle, +} + +/// Controls how soft-wrapped continuation lines are indented. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum IndentAdjustment { + /// No indent - continuation lines start at column 0. + NoIndent, + /// Match the original line's leading whitespace. + #[default] + SameIndent, + /// Add N extra columns of indent (in space-character widths). + ExtraColumns(u32), +} + +/// The GPUI line wrapper, used to wrap lines of text to a given width. +pub struct LineWrapper { + text_system: Arc, + pub(crate) font_id: FontId, + pub(crate) font_size: Pixels, + cached_ascii_char_widths: [Option; 128], + cached_other_char_widths: HashMap, +} + +impl LineWrapper { + /// The maximum indent that can be applied to a line. + pub const MAX_INDENT: u32 = 256; + + pub(crate) fn new(font_id: FontId, font_size: Pixels, text_system: Arc) -> Self { + Self { + text_system, + font_id, + font_size, + cached_ascii_char_widths: [None; 128], + cached_other_char_widths: HashMap::default(), + } + } + + /// Wrap a line of text to the given width with this wrapper's font and font size. + pub fn wrap_line<'a>( + &'a mut self, + fragments: &'a [LineFragment], + wrap_width: Pixels, + indent_adjustment: IndentAdjustment, + ) -> impl Iterator + 'a { + let mut width = px(0.); + let mut first_non_whitespace_ix = None; + let mut base_indent = None; + let mut last_candidate_ix = 0; + let mut last_candidate_width = px(0.); + let mut last_wrap_ix = 0; + let mut prev_c = '\0'; + let mut index = 0; + let mut candidates = fragments + .iter() + .flat_map(move |fragment| fragment.wrap_boundary_candidates()) + .peekable(); + iter::from_fn(move || { + for candidate in candidates.by_ref() { + let ix = index; + index += candidate.len_utf8(); + let mut new_prev_c = prev_c; + let item_width = match candidate { + WrapBoundaryCandidate::Char { character: c } => { + if c == '\n' { + continue; + } + + if Self::is_word_char(c) { + if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + } else { + // CJK may not be space separated, e.g.: `Hello world你好世界` + if c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + new_prev_c = c; + + self.width_for_char(c) + } + WrapBoundaryCandidate::Element { + width: element_width, + .. + } => { + if prev_c == ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + element_width + } + }; + + width += item_width; + if width > wrap_width && ix > last_wrap_ix { + let wrap_at_candidate = + last_candidate_ix > 0 && width - last_candidate_width <= wrap_width; + + let carried_width = if wrap_at_candidate { + width - last_candidate_width + } else { + item_width + }; + + // Compute base indentation from the first non-whitespace character on the line + // and retain it for all subsequent wrap rows. If the line begins with leading + // whitespace that wraps before any non-whitespace character (or is all whitespace), + // base_indent remains None so continuation rows within the leading whitespace + // do not receive ExtraColumns indentation and degrade into cascading short rows. + if base_indent.is_none() { + if let Some(first_non_whitespace_ix) = first_non_whitespace_ix { + base_indent = + Some(Self::MAX_INDENT.min( + first_non_whitespace_ix.saturating_sub(last_wrap_ix) as u32, + )); + } + } + + let next_indent = match indent_adjustment { + IndentAdjustment::NoIndent => 0, + IndentAdjustment::SameIndent => base_indent.unwrap_or(0), + IndentAdjustment::ExtraColumns(extra) => { + if let Some(base_indent) = base_indent { + let candidate = base_indent.saturating_add(extra); + let candidate_indent_width = + self.width_for_char(' ') * candidate as f32; + // Reserve headroom for any carried suffix from an earlier word boundary + // (and at least 2 columns for a full-width character) so the continuation + // line does not immediately exceed wrap width. + let min_headroom = + carried_width.max(self.width_for_char(' ') * 2.0); + if candidate_indent_width + min_headroom > wrap_width { + 0 + } else { + Self::MAX_INDENT.min(candidate) + } + } else { + 0 + } + } + }; + + if wrap_at_candidate { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + } else { + last_wrap_ix = ix; + width = item_width; + } + last_candidate_ix = 0; + + width += self.width_for_char(' ') * next_indent as f32; + + return Some(Boundary::new(last_wrap_ix, next_indent)); + } + + prev_c = new_prev_c; + } + + None + }) + } + + /// Determines if a line should be truncated based on its width. + /// + /// Returns the truncation index in `line`. + pub fn should_truncate_line( + &mut self, + line: &str, + truncate_width: Pixels, + truncation_affix: &str, + truncate_from: TruncateFrom, + ) -> Option { + let mut width = px(0.); + let suffix_width = truncation_affix + .chars() + .map(|c| self.width_for_char(c)) + .fold(px(0.0), |a, x| a + x); + let mut truncate_ix = 0; + + match truncate_from { + TruncateFrom::Start => { + for (ix, c) in line.char_indices().rev() { + if width + suffix_width < truncate_width { + truncate_ix = ix; + } + + let char_width = self.width_for_char(c); + width += char_width; + + if width.floor() > truncate_width { + return Some(truncate_ix); + } + } + } + TruncateFrom::End => { + for (ix, c) in line.char_indices() { + if width + suffix_width < truncate_width { + truncate_ix = ix; + } + + let char_width = self.width_for_char(c); + width += char_width; + + if width.floor() > truncate_width { + return Some(truncate_ix); + } + } + } + TruncateFrom::Middle => {} + } + + None + } + + fn should_truncate_line_middle( + &mut self, + line: &str, + truncate_width: Pixels, + truncation_affix: &str, + ) -> Option<(usize, usize)> { + let suffix_width = truncation_affix + .chars() + .map(|c| self.width_for_char(c)) + .fold(px(0.0), |a, x| a + x); + + let total_width: Pixels = line + .chars() + .map(|c| self.width_for_char(c)) + .fold(px(0.0), |a, x| a + x); + + if total_width <= truncate_width { + return None; + } + + let content_budget = truncate_width - suffix_width; + if content_budget <= px(0.) { + return Some((0, line.len())); + } + + let front_budget = content_budget * (2.0 / 3.0); + let back_budget = content_budget - front_budget; + + let mut front_width = px(0.); + let mut front_end_ix = 0usize; + for (ix, c) in line.char_indices() { + let char_width = self.width_for_char(c); + if front_width + char_width > front_budget { + break; + } + front_width += char_width; + front_end_ix = ix + c.len_utf8(); + } + + let mut back_width = px(0.); + let mut back_start_ix = line.len(); + for (ix, c) in line.char_indices().rev() { + let char_width = self.width_for_char(c); + if back_width + char_width > back_budget { + break; + } + back_width += char_width; + back_start_ix = ix; + } + + if front_end_ix >= back_start_ix { + return Some((0, line.len())); + } + + Some((front_end_ix, back_start_ix)) + } + + /// Truncate a line of text to the given width with this wrapper's font and font size. + pub fn truncate_line<'a>( + &mut self, + line: SharedString, + truncate_width: Pixels, + truncation_affix: &str, + runs: &'a [TextRun], + truncate_from: TruncateFrom, + ) -> (SharedString, Cow<'a, [TextRun]>) { + if truncate_from == TruncateFrom::Middle { + if let Some((front_end_ix, back_start_ix)) = + self.should_truncate_line_middle(&line, truncate_width, truncation_affix) + { + let result = SharedString::from(format!( + "{}{truncation_affix}{}", + &line[..front_end_ix], + &line[back_start_ix..] + )); + let mut runs = runs.to_vec(); + update_runs_after_middle_truncation( + truncation_affix, + &mut runs, + front_end_ix, + back_start_ix, + ); + return (result, Cow::Owned(runs)); + } else { + return (line, Cow::Borrowed(runs)); + } + } + + if let Some(truncate_ix) = + self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from) + { + let result = match truncate_from { + TruncateFrom::Start => SharedString::from(format!( + "{truncation_affix}{}", + &line[line.ceil_char_boundary(truncate_ix + 1)..] + )), + TruncateFrom::End => SharedString::from(format!( + "{}{truncation_affix}", + line[..truncate_ix] + .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()) + )), + TruncateFrom::Middle => unreachable!("Middle truncation is handled above"), + }; + let mut runs = runs.to_vec(); + update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from); + (result, Cow::Owned(runs)) + } else { + (line, Cow::Borrowed(runs)) + } + } + + /// Truncate text to fit within a given number of wrapped lines. + /// + /// Unlike `truncate_line` which treats the text as a flat width budget + /// (`width * max_lines`), this method accounts for word-boundary wrapping: + /// it walks through characters once, tracking wrap boundaries and the + /// truncation point simultaneously. When text overflows on the last + /// allowed line, it truncates there and appends the affix. + /// + /// For `max_lines == 1`, this delegates to `truncate_line`. + pub fn truncate_wrapped_line<'a>( + &mut self, + text: SharedString, + wrap_width: Pixels, + max_lines: usize, + truncation_affix: &str, + runs: &'a [TextRun], + truncate_from: TruncateFrom, + ) -> (SharedString, Cow<'a, [TextRun]>) { + if max_lines <= 1 || truncate_from == TruncateFrom::Start { + return self.truncate_line( + text, + wrap_width * max_lines, + truncation_affix, + runs, + truncate_from, + ); + } + if truncate_from == TruncateFrom::Middle { + return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from); + } + + let affix_width: Pixels = truncation_affix + .chars() + .map(|c| self.width_for_char(c)) + .sum(); + + let mut width = px(0.); + let mut line = 0usize; + let mut first_non_whitespace_ix = None; + let mut last_candidate_ix = 0usize; + let mut last_candidate_width = px(0.); + let mut last_wrap_ix = 0usize; + let mut prev_c = '\0'; + let mut indent: Option = None; + let mut truncate_ix = 0usize; + + for (ix, c) in text.char_indices() { + if c == '\n' { + if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() { + // Newline on the last allowed line with real content + // below. Truncate here. + let truncated = text[..truncate_ix] + .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()); + let result = SharedString::from(format!("{truncated}{truncation_affix}")); + let mut runs = runs.to_vec(); + update_runs_after_truncation( + &result, + truncation_affix, + &mut runs, + TruncateFrom::End, + ); + return (result, Cow::Owned(runs)); + } + + // Newline before the last line: it consumes a line. + line += 1; + width = px(0.); + first_non_whitespace_ix = None; + last_candidate_ix = 0; + last_candidate_width = px(0.); + last_wrap_ix = ix + 1; + prev_c = '\0'; + indent = None; + truncate_ix = ix + 1; + continue; + } + + let char_width = self.width_for_char(c); + + if Self::is_word_char(c) { + if prev_c == ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + } else if c != ' ' && first_non_whitespace_ix.is_some() { + last_candidate_ix = ix; + last_candidate_width = width; + } + + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + + width += char_width; + + if line < max_lines - 1 { + // Before the last line: replicate wrap_line's boundary logic. + if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) { + indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32)); + } + + if last_candidate_ix > last_wrap_ix { + last_wrap_ix = last_candidate_ix; + width -= last_candidate_width; + last_candidate_ix = 0; + } else { + last_wrap_ix = ix; + width = char_width; + } + + if let Some(ind) = indent { + width += self.width_for_char(' ') * ind as f32; + } + + line += 1; + truncate_ix = last_wrap_ix; + } + } else { + // On the last line: track the furthest point where the affix + // still fits, and stop as soon as the line overflows. + if width + affix_width <= wrap_width { + truncate_ix = ix + c.len_utf8(); + } + + if width > wrap_width { + let truncated = text[..truncate_ix] + .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation()); + let result = SharedString::from(format!("{truncated}{truncation_affix}")); + let mut runs = runs.to_vec(); + update_runs_after_truncation( + &result, + truncation_affix, + &mut runs, + TruncateFrom::End, + ); + return (result, Cow::Owned(runs)); + } + } + + prev_c = c; + } + + // Text fits within max_lines without truncation. + (text, Cow::Borrowed(runs)) + } + + /// Any character in this list should be treated as a word character, + /// meaning it can be part of a word that should not be wrapped. + pub(crate) fn is_word_char(c: char) -> bool { + // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc. + c.is_ascii_alphanumeric() || + // Latin script in Unicode for French, German, Spanish, etc. + // Latin-1 Supplement + // https://en.wikipedia.org/wiki/Latin-1_Supplement + matches!(c, '\u{00C0}'..='\u{00FF}') || + // Latin Extended-A + // https://en.wikipedia.org/wiki/Latin_Extended-A + matches!(c, '\u{0100}'..='\u{017F}') || + // Latin Extended-B + // https://en.wikipedia.org/wiki/Latin_Extended-B + matches!(c, '\u{0180}'..='\u{024F}') || + // Cyrillic for Russian, Ukrainian, etc. + // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode + matches!(c, '\u{0400}'..='\u{04FF}') || + + // Vietnamese (https://vietunicode.sourceforge.net/charset/) + matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional + matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks + + // Bengali (https://en.wikipedia.org/wiki/Bengali_(Unicode_block)) + matches!(c, '\u{0980}'..='\u{09FF}') || + + // Some other known special characters that should be treated as word characters, + // e.g. `a-b`, `var_name`, `I'm`/`won’t`, '@mention`, `#hashtag`, `100%`, `3.1415`, + // `2^3`, `a~b`, `a=1`, `Self::new`, etc. Trailing punctuation like `,`, `.`, `:`, `;` + // is included so it stays attached to the preceding word when wrapping. + matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') || + // Closing punctuation never starts a line (UAX #14 LB13: no break + // before `!`, `)`, `]`, `}`, closing quotes or an ellipsis) — `plz!`, + // `see)`, `quoted”` wrap as one word instead of orphaning the mark on + // the next line. `/` and `?` stay break opportunities so long paths + // and URLs (`a/b`, `foo?b=2`) can wrap. + matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') || + // `⋯` character is special used in Zed, to keep this at the end of the line. + matches!(c, '⋯') || + + // Non-breaking glue characters + matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}') + } + + #[inline(always)] + fn width_for_char(&mut self, c: char) -> Pixels { + if (c as u32) < 128 { + if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] { + cached_width + } else { + let width = self + .text_system + .layout_width(self.font_id, self.font_size, c); + self.cached_ascii_char_widths[c as usize] = Some(width); + width + } + } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) { + *cached_width + } else { + let width = self + .text_system + .layout_width(self.font_id, self.font_size, c); + self.cached_other_char_widths.insert(c, width); + width + } + } +} + +fn update_runs_after_truncation( + result: &str, + ellipsis: &str, + runs: &mut Vec, + truncate_from: TruncateFrom, +) { + let mut truncate_at = result.len() - ellipsis.len(); + match truncate_from { + TruncateFrom::Start => { + for (run_index, run) in runs.iter_mut().enumerate().rev() { + if run.len <= truncate_at { + truncate_at -= run.len; + } else { + run.len = truncate_at + ellipsis.len(); + runs.splice(..run_index, std::iter::empty()); + break; + } + } + } + TruncateFrom::End => { + for (run_index, run) in runs.iter_mut().enumerate() { + if run.len <= truncate_at { + truncate_at -= run.len; + } else { + run.len = truncate_at + ellipsis.len(); + runs.truncate(run_index + 1); + break; + } + } + } + TruncateFrom::Middle => { + unreachable!("Middle truncation calls this function with TruncateFrom::End directly") + } + } +} + +fn update_runs_after_middle_truncation( + ellipsis: &str, + runs: &mut Vec, + front_end_ix: usize, + back_start_ix: usize, +) { + let original_runs = std::mem::take(runs); + let mut result_runs: Vec = Vec::with_capacity(original_runs.len()); + + // Front segment [0, front_end_ix) + ellipsis: walk forward until the run + // that straddles or ends at front_end_ix, then extend that run's length + // to include the ellipsis. + let mut front_remaining = front_end_ix; + let mut front_done = false; + for run in &original_runs { + if front_done { + break; + } + if run.len <= front_remaining { + result_runs.push(run.clone()); + front_remaining -= run.len; + } else { + let mut partial = run.clone(); + partial.len = front_remaining + ellipsis.len(); + result_runs.push(partial); + front_done = true; + } + } + if !front_done { + // front_end_ix landed exactly on a run boundary; append ellipsis to + // the last front run (or, if the front is empty, to the first back run). + if let Some(last) = result_runs.last_mut() { + last.len += ellipsis.len(); + } else if let Some(first) = original_runs.first() { + let mut affix_run = first.clone(); + affix_run.len = ellipsis.len(); + result_runs.push(affix_run); + } + } + + // Back segment [back_start_ix, original.len()): skip runs entirely in the + // removed middle, keep the rest. + let mut byte_pos = 0usize; + for run in &original_runs { + let run_end = byte_pos + run.len; + if run_end > back_start_ix { + if byte_pos < back_start_ix { + // Run straddles back_start_ix; keep only the tail. + let mut partial = run.clone(); + partial.len = run_end - back_start_ix; + result_runs.push(partial); + } else { + result_runs.push(run.clone()); + } + } + byte_pos = run_end; + } + + *runs = result_runs; +} + +/// A fragment of a line that can be wrapped. +pub enum LineFragment<'a> { + /// A text fragment consisting of characters. + Text { + /// The text content of the fragment. + text: &'a str, + }, + /// A non-text element with a fixed width. + Element { + /// The width of the element in pixels. + width: Pixels, + /// The UTF-8 encoded length of the element. + len_utf8: usize, + }, +} + +impl<'a> LineFragment<'a> { + /// Creates a new text fragment from the given text. + pub fn text(text: &'a str) -> Self { + LineFragment::Text { text } + } + + /// Creates a new non-text element with the given width and UTF-8 encoded length. + pub fn element(width: Pixels, len_utf8: usize) -> Self { + LineFragment::Element { width, len_utf8 } + } + + fn wrap_boundary_candidates(&self) -> impl Iterator { + let text = match self { + LineFragment::Text { text } => text, + LineFragment::Element { .. } => "\0", + }; + text.chars().map(move |character| { + if let LineFragment::Element { width, len_utf8 } = self { + WrapBoundaryCandidate::Element { + width: *width, + len_utf8: *len_utf8, + } + } else { + WrapBoundaryCandidate::Char { character } + } + }) + } +} + +enum WrapBoundaryCandidate { + Char { character: char }, + Element { width: Pixels, len_utf8: usize }, +} + +impl WrapBoundaryCandidate { + pub fn len_utf8(&self) -> usize { + match self { + WrapBoundaryCandidate::Char { character } => character.len_utf8(), + WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len, + } + } +} + +/// A boundary between two lines of text. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + /// The index of the last character in a line + pub ix: usize, + /// The indent of the next line. + pub next_indent: u32, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font}; + #[cfg(target_os = "macos")] + use crate::{TextRun, WindowTextSystem, WrapBoundary}; + + fn build_wrapper() -> LineWrapper { + let dispatcher = TestDispatcher::new(0); + let cx = TestAppContext::build(dispatcher, None); + let id = cx.text_system().resolve_font(&font(".ZedMono")); + LineWrapper::new(id, px(16.), cx.text_system().clone()) + } + + fn generate_test_runs(input_run_len: &[usize]) -> Vec { + input_run_len + .iter() + .map(|run_len| TextRun { + len: *run_len, + font: Font { + family: "Dummy".into(), + features: FontFeatures::default(), + fallbacks: None, + weight: FontWeight::default(), + style: FontStyle::Normal, + }, + ..Default::default() + }) + .collect() + } + + #[test] + fn test_wrap_line() { + let mut wrapper = build_wrapper(); + + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text("aa bbb cccc ddddd eeee")], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text("aaa aaaaaaaaaaaaaaaaaa")], + px(72.0), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], + ); + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(" aaaaaaa")], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] + ); + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(" ")], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(" aaaaaaaaaaaaaa")], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] + ); + + // Test wrapping multiple text fragments + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("aa bbb "), + LineFragment::text("cccc ddddd eeee") + ], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + + // Test wrapping with a mix of text and element fragments + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("aa "), + LineFragment::element(px(20.), 1), + LineFragment::text(" bbb "), + LineFragment::element(px(30.), 1), + LineFragment::text(" cccc") + ], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(5, 0), + Boundary::new(9, 0), + Boundary::new(11, 0) + ], + ); + + // Test with element at the beginning and text afterward + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::element(px(50.), 1), + LineFragment::text(" aaaa bbbb cccc dddd") + ], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(2, 0), + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(17, 0) + ], + ); + + // Test with a large element that forces wrapping by itself + assert_eq!( + wrapper + .wrap_line( + &[ + LineFragment::text("short text "), + LineFragment::element(px(100.), 1), + LineFragment::text(" more text") + ], + px(72.), + IndentAdjustment::default() + ) + .collect::>(), + &[ + Boundary::new(6, 0), + Boundary::new(11, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], + ); + + // Test with non-breaking glue characters + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text("a\u{202F}b\u{00A0}c\u{2011}d e")], + px(72.0), + IndentAdjustment::default() + ) + .collect::>(), + &[Boundary::new(12, 0),], // special chars above take up 3, 2 and 3 bytes, so boundary ends up at 12 + ); + } + + #[test] + fn test_truncate_line_end() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + expected: &'static str, + ellipsis: &str, + ) { + let dummy_run_lens = vec![text.len()]; + let dummy_runs = generate_test_runs(&dummy_run_lens); + let (result, dummy_runs) = wrapper.truncate_line( + text.into(), + px(220.), + ellipsis, + &dummy_runs, + TruncateFrom::End, + ); + assert_eq!(result, expected); + assert_eq!(dummy_runs.first().unwrap().len, result.len()); + } + + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eeee", + "", + ); + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eee…", + "…", + ); + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc dddd......", + "......", + ); + perform_test( + &mut wrapper, + "aa bbb cccc 🦀🦀🦀🦀🦀 eeee ffff gggg", + "aa bbb cccc 🦀🦀🦀🦀…", + "…", + ); + } + + #[test] + fn test_truncate_line_start() { + let mut wrapper = build_wrapper(); + + #[track_caller] + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + expected: &'static str, + ellipsis: &str, + ) { + let dummy_run_lens = vec![text.len()]; + let dummy_runs = generate_test_runs(&dummy_run_lens); + let (result, dummy_runs) = wrapper.truncate_line( + text.into(), + px(220.), + ellipsis, + &dummy_runs, + TruncateFrom::Start, + ); + assert_eq!(result, expected); + assert_eq!(dummy_runs.first().unwrap().len, result.len()); + } + + perform_test( + &mut wrapper, + "aaaa bbbb cccc ddddd eeee fff gg", + "cccc ddddd eeee fff gg", + "", + ); + perform_test( + &mut wrapper, + "aaaa bbbb cccc ddddd eeee fff gg", + "…ccc ddddd eeee fff gg", + "…", + ); + perform_test( + &mut wrapper, + "aaaa bbbb cccc ddddd eeee fff gg", + "......dddd eeee fff gg", + "......", + ); + perform_test( + &mut wrapper, + "aaaa bbbb cccc 🦀🦀🦀🦀🦀 eeee fff gg", + "…🦀🦀🦀🦀 eeee fff gg", + "…", + ); + } + + #[test] + fn test_truncate_multiple_runs_end() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + expected: &str, + run_lens: &[usize], + result_run_len: &[usize], + line_width: Pixels, + ) { + let dummy_runs = generate_test_runs(run_lens); + let (result, dummy_runs) = + wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End); + assert_eq!(result, expected); + for (run, result_len) in dummy_runs.iter().zip(result_run_len) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.)); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdef…", + &[4, 4, 4], + &[4, 5], + px(70.), + ); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdefgh…", + &[4, 4, 4], + &[4, 4, 3], + px(90.), + ); + } + + #[test] + fn test_truncate_multiple_runs_start() { + let mut wrapper = build_wrapper(); + + #[track_caller] + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + expected: &str, + run_lens: &[usize], + result_run_len: &[usize], + line_width: Pixels, + ) { + let dummy_runs = generate_test_runs(run_lens); + let (result, dummy_runs) = wrapper.truncate_line( + text.into(), + line_width, + "…", + &dummy_runs, + TruncateFrom::Start, + ); + assert_eq!(result, expected); + for (run, result_len) in dummy_runs.iter().zip(result_run_len) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: …ijkl (truncate_at = 9) + // Run res: Run0 { string: …ijkl, len: 7, ... } + perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.)); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: …ghijkl (truncate_at = 7) + // Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len: + // 4, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "…ghijkl", + &[4, 4, 4], + &[5, 4], + px(70.), + ); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 3) + // Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: ijkl, len: 4, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "…efghijkl", + &[4, 4, 4], + &[3, 4, 4], + px(90.), + ); + } + + #[test] + fn test_update_run_after_truncation_end() { + fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) { + let mut dummy_runs = generate_test_runs(run_lens); + update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End); + for (run, result_len) in dummy_runs.iter().zip(result_run_lens) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test("abcd…", &[12], &[7]); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test("abcdef…", &[4, 4, 4], &[4, 5]); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]); + } + + #[test] + fn test_is_word_char() { + #[track_caller] + fn assert_word(word: &str) { + for c in word.chars() { + assert!( + LineWrapper::is_word_char(c), + "assertion failed for '{}' (unicode 0x{:x})", + c, + c as u32 + ); + } + } + + #[track_caller] + fn assert_not_word(word: &str) { + let found = word.chars().any(|c| !LineWrapper::is_word_char(c)); + assert!(found, "assertion failed for '{}'", word); + } + + assert_word("Hello123"); + assert_word("non-English"); + assert_word("var_name"); + assert_word("123456"); + assert_word("3.1415"); + assert_word("10^2"); + assert_word("1~2"); + assert_word("100%"); + assert_word("@mention"); + assert_word("#hashtag"); + assert_word("$variable"); + assert_word("a=1"); + assert_word("Self::is_word_char"); + assert_word("on;"); + assert_word("more⋯"); + assert_word("won’t"); + assert_word("‘twas"); + assert_word("plz!"); + assert_word("see)"); + assert_word("quoted”"); + assert_word("well…"); + + // Space + assert_not_word("foo bar"); + + // URL case + assert_word("github.com"); + assert_not_word("zed-industries/zed"); + assert_not_word("zed-industries\\zed"); + assert_not_word("a=1&b=2"); + assert_not_word("foo?b=2"); + + // Latin-1 Supplement + assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ"); + // Latin Extended-A + assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď"); + // Latin Extended-B + assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ"); + // Cyrillic + assert_word("АБВГДЕЖЗИЙКЛМНОП"); + // Vietnamese (https://github.com/zed-industries/zed/issues/23245) + assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng"); + // Bengali + assert_word("গিয়েছিলেন"); + assert_word("ছেলে"); + assert_word("হচ্ছিল"); + + // non-word characters + assert_not_word("你好"); + assert_not_word("안녕하세요"); + assert_not_word("こんにちは"); + assert_not_word("😀😁😂"); + assert_not_word("()[]{}<>"); + + // Non-breaking ("Glue") characters, see https://www.unicode.org/reports/tr14/ + // (https://github.com/zed-industries/zed/issues/59664) + assert_word("\u{202F}"); // NNBSP " " + assert_word("\u{00A0}"); // NBSP " " + assert_word("\u{2011}"); // NBH "‑" + } + + // For compatibility with the test macro + #[cfg(target_os = "macos")] + use crate as gpui; + + // These seem to vary wildly based on the text system. + #[cfg(target_os = "macos")] + #[crate::test] + fn test_wrap_shaped_line(cx: &mut TestAppContext) { + cx.update(|cx| { + let text_system = WindowTextSystem::new(cx.text_system().clone()); + + let normal = TextRun { + len: 0, + font: font("Helvetica"), + color: Default::default(), + underline: Default::default(), + ..Default::default() + }; + let bold = TextRun { + len: 0, + font: font("Helvetica").bold(), + ..Default::default() + }; + + let text = "aa bbb cccc ddddd eeee".into(); + let lines = text_system + .shape_text( + text, + px(16.), + &[ + normal.with_len(4), + bold.with_len(5), + normal.with_len(6), + bold.with_len(1), + normal.with_len(7), + ], + Some(px(72.)), + None, + ) + .unwrap(); + + assert_eq!( + lines[0].layout.wrap_boundaries(), + &[ + WrapBoundary { + run_ix: 0, + glyph_ix: 7 + }, + WrapBoundary { + run_ix: 0, + glyph_ix: 12 + }, + WrapBoundary { + run_ix: 0, + glyph_ix: 18 + } + ], + ); + }); + } + + #[test] + fn test_multiline_truncation_fits_within_wrapped_lines() { + let mut wrapper = build_wrapper(); + + // With .ZedMono at 16px, each char is 9.6px wide. + // wrap_width = 72px fits ~7 chars per line. + // + // "aa bbbbbb cccccc dddddd eeee ffff" with wrap_width=72px wraps as: + // Line 1: "aa " (28.8px, wraps because "bbbbbb" won't fit) + // Line 2: "bbbbbb " (67.2px) + // Line 3: "cccccc " (67.2px) + // ... + // + // truncate_wrapped_line should wrap first to find line 2 starts at + // "bbbbbb...", then truncate only that line to fit with ellipsis. + let text: &str = "aa bbbbbb cccccc dddddd eeee ffff"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (truncated, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + // The truncated text, when wrapped, must fit within max_lines lines. + let wrap_count = wrapper + .wrap_line( + &[LineFragment::text(&truncated)], + wrap_width, + IndentAdjustment::default(), + ) + .count(); + + assert!( + wrap_count < max_lines, + "Truncated text '{}' wraps into {} visual lines, expected at most {}", + truncated, + wrap_count + 1, + max_lines + ); + + // The truncated text should end with the ellipsis. + assert!( + truncated.ends_with('\u{2026}'), + "Truncated text '{}' should end with ellipsis", + truncated + ); + } + + #[test] + fn test_multiline_truncation_no_truncation_needed() { + let mut wrapper = build_wrapper(); + + // Text that fits in 2 lines shouldn't be truncated. + // Line 1: "aa bbb " (67.2px), Line 2: "cccccc" (57.6px) + let text: &str = "aa bbb cccccc"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (result, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + assert_eq!( + result.as_ref(), + text, + "Text that fits should not be modified" + ); + } + + #[test] + fn test_multiline_truncation_three_lines() { + let mut wrapper = build_wrapper(); + + let text: &str = "aa bbb cccc ddddd eeee ffff gggg hhhh iiii jjjj"; + let wrap_width = px(72.); + let max_lines: usize = 3; + + let runs = generate_test_runs(&[text.len()]); + let (truncated, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + let wrap_count = wrapper + .wrap_line( + &[LineFragment::text(&truncated)], + wrap_width, + IndentAdjustment::default(), + ) + .count(); + + assert!( + wrap_count < max_lines, + "Truncated text '{}' wraps into {} visual lines, expected at most {}", + truncated, + wrap_count + 1, + max_lines + ); + + assert!( + truncated.ends_with('\u{2026}'), + "Truncated text '{}' should end with ellipsis", + truncated + ); + } + + #[test] + fn test_multiline_truncation_with_newlines() { + let mut wrapper = build_wrapper(); + + // "hello\nworld foo bar baz" with line_clamp(2): + // shape_text splits on \n, giving physical lines "hello" and + // "world foo bar baz". The newline consumes line 1, so the + // second physical line should be truncated on line 2. + let text: &str = "hello\nworld foo bar baz"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (truncated, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + // The newline should be preserved. + let parts: Vec<&str> = truncated.splitn(2, '\n').collect(); + assert_eq!( + parts.len(), + 2, + "Newline should be preserved: '{}'", + truncated + ); + assert_eq!(parts[0], "hello"); + + // The second line should fit within wrap_width and end with ellipsis. + let second_line_width: Pixels = parts[1].chars().map(|c| wrapper.width_for_char(c)).sum(); + assert!( + second_line_width <= wrap_width, + "Second line '{}' ({}px) exceeds wrap_width ({}px)", + parts[1], + second_line_width, + wrap_width + ); + assert!( + truncated.ends_with('\u{2026}'), + "Should end with ellipsis: '{}'", + truncated + ); + } + + #[test] + fn test_multiline_truncation_newline_on_last_line() { + let mut wrapper = build_wrapper(); + + // "hello\nworld\nmore" with line_clamp(2): + // Line 1: "hello", Line 2: "world" — but there's a third line, + // so line 2 should be truncated with ellipsis. + let text: &str = "hello\nworld\nmore"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (truncated, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + let parts: Vec<&str> = truncated.splitn(2, '\n').collect(); + assert_eq!(parts[0], "hello"); + assert!( + truncated.ends_with('\u{2026}'), + "Should end with ellipsis since there's more content: '{}'", + truncated + ); + } + + #[test] + fn test_truncate_line_middle() { + let mut wrapper = build_wrapper(); + + // No truncation when text fits within a very wide budget. + let short_text = "hello world"; + let runs = generate_test_runs(&[short_text.len()]); + let (result, result_runs) = wrapper.truncate_line( + short_text.into(), + px(10000.), + "…", + &runs, + TruncateFrom::Middle, + ); + assert_eq!(result.as_ref(), short_text); + assert_eq!(result_runs.len(), 1); + assert_eq!(result_runs[0].len, short_text.len()); + + // Basic middle truncation: long string with px(100.) budget. + let long_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"; + let runs = generate_test_runs(&[long_text.len()]); + let (result, _result_runs) = + wrapper.truncate_line(long_text.into(), px(100.), "…", &runs, TruncateFrom::Middle); + assert!( + result.contains('…'), + "Middle-truncated result should contain '…', got: '{}'", + result + ); + assert!( + result.chars().count() < long_text.chars().count(), + "Middle-truncated result should be shorter than original" + ); + assert_eq!( + result.chars().next(), + long_text.chars().next(), + "Result should start with the same first character as original" + ); + assert_eq!( + result.chars().last(), + long_text.chars().last(), + "Result should end with the same last character as original" + ); + + // Degenerate case: budget so narrow that middle truncation cannot find a valid split. + // Still show the truncation affix instead of returning the original overflowing text. + let text = "abcdef"; + let runs = generate_test_runs(&[text.len()]); + let (result, result_runs) = + wrapper.truncate_line(text.into(), px(1.), "…", &runs, TruncateFrom::Middle); + assert_eq!(result.as_ref(), "…"); + assert_eq!(result_runs.len(), 1); + assert_eq!(result_runs[0].len, "…".len()); + + // Run adjustment correctness: multiple runs across the string. + // Verify that the returned runs' lengths sum to result.len(). + let multi_run_text = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"; + let run_lens = [20, 20, multi_run_text.len() - 40]; + let runs = generate_test_runs(&run_lens); + let (result, result_runs) = wrapper.truncate_line( + multi_run_text.into(), + px(100.), + "…", + &runs, + TruncateFrom::Middle, + ); + let total_run_len: usize = result_runs.iter().map(|r| r.len).sum(); + assert_eq!( + total_run_len, + result.len(), + "Sum of run lengths ({}) should equal result byte length ({})", + total_run_len, + result.len() + ); + } + + #[test] + fn test_multiline_truncation_trailing_newline() { + let mut wrapper = build_wrapper(); + + // "hello\nworld\n" with line_clamp(2): + // The trailing newline has no content after it, so no ellipsis. + let text: &str = "hello\nworld\n"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (result, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + assert!( + !result.ends_with('\u{2026}'), + "Trailing newline with no content should not add ellipsis: '{}'", + result + ); + } + + #[test] + fn test_multiline_truncation_newline_fits_exactly() { + let mut wrapper = build_wrapper(); + + // "hello\nworld" with line_clamp(2): + // Exactly 2 lines, no truncation needed. + let text: &str = "hello\nworld"; + let wrap_width = px(72.); + let max_lines: usize = 2; + + let runs = generate_test_runs(&[text.len()]); + let (result, _) = wrapper.truncate_wrapped_line( + text.into(), + wrap_width, + max_lines, + "\u{2026}", + &runs, + TruncateFrom::End, + ); + + assert_eq!( + result.as_ref(), + text, + "Text that fits exactly should not be modified: '{}'", + result + ); + } + + #[test] + fn test_extra_columns_overflow_guard() { + let mut wrapper = build_wrapper(); + let space_width = wrapper.width_for_char(' '); + + // 6 spaces indent, wrap width 10 columns. + let text = " ab cd ef gh"; + let wrap_width = space_width * 10.0; + + // When base_indent + extra overflows wrap width (6 + 8 + 2 > 10), + // indent must fall back to 0 instead of degrading to one character per row. + // + // Expected wrapped lines (10 columns): + // | ab | (row 0: 6 spaces + "ab ", len 9) + // |cd ef gh | (row 1: 0 indent + "cd ef gh", len 8) + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(text)], + wrap_width, + IndentAdjustment::ExtraColumns(8), + ) + .collect::>(), + &[Boundary::new(9, 0)] + ); + + // When base_indent + extra fits within wrap width (6 + 2 + 2 <= 10), + // the extra indent is applied and not clamped. + // + // Expected wrapped lines (10 columns): + // | ab | (row 0: 6 spaces + "ab ", len 9) + // | cd| (row 1: 8 spaces + "cd", len 10) + // | ef| (row 2: 8 spaces + "ef", len 10) + // | gh| (row 3: 8 spaces + "gh", len 10) + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(text)], + wrap_width, + IndentAdjustment::ExtraColumns(2), + ) + .collect::>(), + &[ + Boundary::new(9, 8), + Boundary::new(11, 8), + Boundary::new(13, 8), + Boundary::new(15, 8), + ] + ); + + // When text contains full-width (two-column) glyphs, reserving headroom for a + // two-column character (candidate + 2 > wrap_width) ensures that candidate + // indents leaving only 1 column cannot accept the indent and overflow. + // + // 1 space indent, wrap width 10 columns. + // " 🦀🦀🦀🦀🦀🦀🦀🦀" with extra=8 (extra_two with tab_size=4): + // candidate = 1 + 8 = 9. + // With +2 headroom (9 + 2 > 10), indent falls back to 0. + // + // Expected wrapped lines (10 columns): + // | 🦀🦀🦀🦀 | (row 0: 1 space + 4 two-column glyphs, 9 cols) + // |🦀🦀🦀🦀 | (row 1: 0 indent + 4 two-column glyphs, 8 cols) + let full_width_text = " 🦀🦀🦀🦀🦀🦀🦀🦀"; + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(full_width_text)], + wrap_width, + IndentAdjustment::ExtraColumns(8), + ) + .collect::>(), + &[Boundary::new(17, 0)] + ); + + // When the carried suffix fits within wrap width alongside extra indent + // (1 indent + 9 carried suffix = 10 <= 10), the extra indent is applied. + // + // Expected wrapped lines (10 columns): + // |a | (row 0: "a ", len 2) + // | abcdefghi| (row 1: 1 indent + "abcdefghi", len 10) + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text("a abcdefghi")], + wrap_width, + IndentAdjustment::ExtraColumns(1), + ) + .collect::>(), + &[Boundary::new(2, 1)] + ); + + // When extra indent exceeds wrap width by even 1 column + // (2 indent + 9 carried suffix = 11 > 10), indent falls back to 0. + // + // Expected wrapped lines (10 columns): + // |a | (row 0: "a ", len 2) + // |abcdefghi | (row 1: 0 indent + "abcdefghi", len 9) + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text("a abcdefghi")], + wrap_width, + IndentAdjustment::ExtraColumns(2), + ) + .collect::>(), + &[Boundary::new(2, 0)] + ); + + // When text wraps at an earlier word boundary, the carried suffix + // must be accounted for so indent + carried_suffix <= wrap_width. + // + // "a abcdefghij" with wrap_width 10 columns, ExtraColumns(8): + // candidate = 0 + 8 = 8. + // carried suffix = "abcdefghi" (9 columns). + // 8 indent + 9 carried suffix = 17 > 10, so indent falls back to 0. + // + // Expected wrapped lines (10 columns): + // |a | (row 0: "a ", len 2) + // |abcdefghij| (row 1: 0 indent + "abcdefghij", len 10) + let carried_text = "a abcdefghij"; + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(carried_text)], + wrap_width, + IndentAdjustment::ExtraColumns(8), + ) + .collect::>(), + &[Boundary::new(2, 0)] + ); + + // When a wider inline element is encountered after the first wrap, + // the overflow guard must evaluate whether the element fits with the extra indent. + // If text continues after the element row and wraps again, subsequent continuation + // lines resume the extra indent if their carried content fits. + // + // "abcdefghijk " followed by an 8-column element and "z 12": + // Row 0: "abcdefghij" (len 10) + // Row 1: "k " (2 cols) with 8 indent (len 10) + // Row 2: element (8 cols) cannot fit with 8 indent (8 + 8 = 16 > 10), + // so indent falls back to 0. Element (8 cols) + "z " (2 cols) = len 10. + // Row 3: "12" (2 cols) fits with 8 indent (8 + 2 = 10 <= 10). + // + // Expected wrapped lines (10 columns): + // |abcdefghij| (row 0: 10 cols) + // | k | (row 1: 8 indent + "k ", len 10) + // |[ELEMENT]z| (row 2: 0 indent + [ELEMENT (8)] + "z ", len 10) + // | 12| (row 3: 8 indent + "12", len 10) + let element_fragments = [ + LineFragment::text("abcdefghijk "), + LineFragment::element(space_width * 8.0, 1), + LineFragment::text("z 12"), + ]; + assert_eq!( + wrapper + .wrap_line( + &element_fragments, + wrap_width, + IndentAdjustment::ExtraColumns(8), + ) + .collect::>(), + &[ + Boundary::new(10, 8), + Boundary::new(12, 0), + Boundary::new(15, 8), + ] + ); + + // A line of only whitespace wrapping before any non-whitespace character + // must not have extra columns added to subsequent rows. + let spaces = " "; // 20 spaces + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(spaces)], + wrap_width, + IndentAdjustment::ExtraColumns(8), + ) + .collect::>(), + &[Boundary::new(10, 0)] + ); + + // When leading whitespace wraps before the first non-whitespace character, + // base_indent should reflect the leading whitespace on the row where non-whitespace begins. + // 14 spaces followed by "ab cd ef gh", wrap width 10: + // Row 0: 10 spaces (len 10) -> wraps at ix 10 with indent 0 + // Row 1: 4 spaces + "ab cd" (len 9) -> wraps at ix 20 + // base_indent is 14 - 10 = 4. + // With ExtraColumns(2), candidate = 4 + 2 = 6. 6 + 2 (headroom) = 8 <= 10. + // Row 2 and subsequent rows get indent 6. + let multiline_indent_text = " ab cd ef gh"; + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(multiline_indent_text)], + wrap_width, + IndentAdjustment::ExtraColumns(2), + ) + .collect::>(), + &[ + Boundary::new(10, 0), + Boundary::new(20, 6), + Boundary::new(23, 6), + ] + ); + + // When a word boundary precedes an oversized carried suffix that itself + // exceeds wrap_width (e.g. text followed by an inline element), wrapping + // at the earlier candidate would force the continuation row to immediately + // overflow even with 0 indent. The wrapper must reject the candidate and + // break before the overflowing item instead. + // + // "aaaaaaaaaaaaaaaaaaa a" (21 chars: 19 'a's, space, 'a') followed by 10-column element: + // Row 0: "aaaaaaaaaa" (10 chars, len 10) -> wraps at ix 10 + // Row 1: "aaaaaaaa" (8 chars) with 2 indent (len 10) -> wraps at ix 18 + // Row 2: "a a" (3 chars) with 2 indent (len 5) + // candidate boundary at ix 20 (space) + // Then element (10 cols): width becomes 5 + 10 = 15 > wrap_width (10). + // Candidate would carry 'a' (1 col) + element (10 cols) = 11 cols > 10. + // Because carried suffix (11) > wrap_width (10), candidate is rejected. + // Wrapper breaks at ix 21 (before element) with indent 0. + // Row 3: element (10 cols, len 10) + let oversized_suffix_fragments = [ + LineFragment::text("aaaaaaaaaaaaaaaaaaa a"), + LineFragment::element(space_width * 10.0, 1), + ]; + assert_eq!( + wrapper + .wrap_line( + &oversized_suffix_fragments, + wrap_width, + IndentAdjustment::ExtraColumns(2), + ) + .collect::>(), + &[ + Boundary::new(10, 2), + Boundary::new(18, 2), + Boundary::new(21, 0), + ] + ); + + assert_eq!( + wrapper + .wrap_line( + &[LineFragment::text(" aaaaaaaa")], + space_width * 4.0, + IndentAdjustment::ExtraColumns(u32::MAX), + ) + .collect::>(), + &[Boundary::new(4, 0), Boundary::new(8, 0)] + ); + } +} diff --git a/vendor/gpui-pre/src/util.rs b/vendor/gpui-pre/src/util.rs new file mode 100644 index 0000000..13ee752 --- /dev/null +++ b/vendor/gpui-pre/src/util.rs @@ -0,0 +1,252 @@ +use crate::{BackgroundExecutor, Task}; +use std::{ + future::Future, + pin::Pin, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, + task, + time::Duration, +}; + +/// A helper trait for building complex objects with imperative conditionals in a fluent style. +pub trait FluentBuilder { + /// Imperatively modify self with the given closure. + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + { + f(self) + } + + /// Conditionally modify self with the given closure. + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { this }) + } + + /// Conditionally modify self with the given closure. + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + else_fn: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { else_fn(this) }) + } + + /// Conditionally unwrap and modify self with the given closure, if the given option is Some. + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } + /// Conditionally unwrap and modify self with the given closure, if the given option is None. + fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if option.is_some() { this } else { then(this) }) + } +} + +/// Extensions for Future types that provide additional combinators and utilities. +pub trait FutureExt { + /// Requires a Future to complete before the specified duration has elapsed. + /// Similar to tokio::timeout. + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized; +} + +impl FutureExt for T { + fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout + where + Self: Sized, + { + WithTimeout { + future: self, + timer: executor.timer(timeout), + } + } +} + +#[pin_project::pin_project] +pub struct WithTimeout { + #[pin] + future: T, + #[pin] + timer: Task<()>, +} + +#[derive(Debug, thiserror::Error)] +#[error("Timed out before future resolved")] +/// Error returned by with_timeout when the timeout duration elapsed before the future resolved +pub struct Timeout; + +impl Future for WithTimeout { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { + let this = self.project(); + + if let task::Poll::Ready(output) = this.future.poll(cx) { + task::Poll::Ready(Ok(output)) + } else if this.timer.poll(cx).is_ready() { + task::Poll::Ready(Err(Timeout)) + } else { + task::Poll::Pending + } + } +} + +/// Increment the given atomic counter if it is not zero. +/// Return the new value of the counter. +pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize { + let mut loaded = counter.load(SeqCst); + loop { + if loaded == 0 { + return 0; + } + match counter.compare_exchange_weak(loaded, loaded + 1, SeqCst, SeqCst) { + Ok(x) => return x + 1, + Err(actual) => loaded = actual, + } + } +} + +/// Rounds to the nearest integer with 0.5 ties toward zero. +#[inline] +pub(crate) fn round_half_toward_zero(value: f32) -> f32 { + (value.abs() - 0.5).ceil().copysign(value) +} + +#[inline] +pub(crate) fn round_half_toward_zero_f64(value: f64) -> f64 { + (value.abs() - 0.5).ceil().copysign(value) +} + +#[inline] +pub(crate) fn round_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { + round_half_toward_zero(logical * scale_factor) +} + +#[inline] +pub(crate) fn round_stroke_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { + if logical == 0.0 { + 0.0 + } else { + round_to_device_pixel(logical.max(0.0), scale_factor).max(1.0) + } +} + +#[inline] +pub(crate) fn floor_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { + (logical * scale_factor).floor() +} + +#[inline] +pub(crate) fn ceil_to_device_pixel(logical: f32, scale_factor: f32) -> f32 { + (logical * scale_factor).ceil() +} + +#[cfg(test)] +mod tests { + use crate::TestAppContext; + + use super::*; + + #[test] + fn test_round_half_toward_zero() { + // Midpoint ties go toward zero + assert_eq!(round_half_toward_zero(0.5), 0.0); + assert_eq!(round_half_toward_zero(1.5), 1.0); + assert_eq!(round_half_toward_zero(2.5), 2.0); + assert_eq!(round_half_toward_zero(-0.5), 0.0); + assert_eq!(round_half_toward_zero(-1.5), -1.0); + assert_eq!(round_half_toward_zero(-2.5), -2.0); + + // Non-midpoint values round to nearest + assert_eq!(round_half_toward_zero(1.5001), 2.0); + assert_eq!(round_half_toward_zero(1.4999), 1.0); + assert_eq!(round_half_toward_zero(-1.5001), -2.0); + assert_eq!(round_half_toward_zero(-1.4999), -1.0); + + // Integers are unchanged + assert_eq!(round_half_toward_zero(0.0), 0.0); + assert_eq!(round_half_toward_zero(3.0), 3.0); + assert_eq!(round_half_toward_zero(-3.0), -3.0); + } + + #[test] + fn test_device_pixel_helpers() { + // Snap uses half-toward-zero: 1.0 * 1.5 = 1.5 ties toward 1.0. + assert_eq!(round_to_device_pixel(1.0, 1.5), 1.0); + // Below the tie rounds down, above rounds up. + assert_eq!(round_to_device_pixel(0.3, 2.0), 1.0); + assert_eq!(round_to_device_pixel(1.4, 1.0), 1.0); + assert_eq!(round_to_device_pixel(1.6, 1.0), 2.0); + + // Stroke uses snap, but clamps non-zero input up to at least 1dp. + assert_eq!(round_stroke_to_device_pixel(0.0, 1.0), 0.0); + assert_eq!(round_stroke_to_device_pixel(0.4, 1.0), 1.0); + assert_eq!(round_stroke_to_device_pixel(0.5, 1.0), 1.0); + assert_eq!(round_stroke_to_device_pixel(1.0, 1.5), 1.0); + assert_eq!(round_stroke_to_device_pixel(1.6, 1.0), 2.0); + + // Cover's near edge floors, far edge ceils. Together they form a strict superset. + assert_eq!(floor_to_device_pixel(0.3, 2.0), 0.0); + assert_eq!(ceil_to_device_pixel(0.3, 2.0), 1.0); + assert_eq!(floor_to_device_pixel(2.1, 1.0), 2.0); + assert_eq!(ceil_to_device_pixel(2.1, 1.0), 3.0); + + // Integer device-pixel inputs are stable under all three. + assert_eq!(round_to_device_pixel(2.0, 2.0), 4.0); + assert_eq!(floor_to_device_pixel(2.0, 2.0), 4.0); + assert_eq!(ceil_to_device_pixel(2.0, 2.0), 4.0); + } + + #[test] + fn test_round_half_toward_zero_f64() { + assert_eq!(round_half_toward_zero_f64(0.5), 0.0); + assert_eq!(round_half_toward_zero_f64(-0.5), 0.0); + assert_eq!(round_half_toward_zero_f64(1.5), 1.0); + assert_eq!(round_half_toward_zero_f64(-1.5), -1.0); + assert_eq!(round_half_toward_zero_f64(2.5001), 3.0); + } + + #[gpui::test] + async fn test_with_timeout(cx: &mut TestAppContext) { + Task::ready(()) + .with_timeout(Duration::from_secs(1), &cx.executor()) + .await + .expect("Timeout should be noop"); + + let long_duration = Duration::from_secs(6000); + let short_duration = Duration::from_secs(1); + cx.executor() + .timer(long_duration) + .with_timeout(short_duration, &cx.executor()) + .await + .expect_err("timeout should have triggered"); + + let fut = cx + .executor() + .timer(long_duration) + .with_timeout(short_duration, &cx.executor()); + cx.executor().advance_clock(short_duration * 2); + futures::FutureExt::now_or_never(fut) + .unwrap_or_else(|| panic!("timeout should have triggered")) + .expect_err("timeout"); + } +} diff --git a/vendor/gpui-pre/src/view.rs b/vendor/gpui-pre/src/view.rs new file mode 100644 index 0000000..f24a4da --- /dev/null +++ b/vendor/gpui-pre/src/view.rs @@ -0,0 +1,593 @@ +use crate::{ + AnyElement, AnyEntity, AnyWeakEntity, App, AvailableSpace, Bounds, ContentMask, Context, + Element, ElementId, Entity, EntityId, GlobalElementId, InspectorElementId, IntoElement, + LayoutId, PaintIndex, Pixels, PrepaintStateIndex, Render, RenderOnce, Size, Style, + StyleRefinement, TextStyle, WeakEntity, +}; +use crate::{Empty, Window}; +use anyhow::Result; +use collections::FxHashSet; +use refineable::Refineable; +use std::mem; +use std::{ + any::{TypeId, type_name}, + fmt, + ops::Range, +}; + +/// A dynamically-typed view handle that can be downcast to a specific `Entity`. +/// +/// This is the type-erased counterpart to [`ViewElement`]: it holds an entity plus +/// a function pointer to its render, and is itself a [`View`], so embedding it as an +/// element goes through the same [`ViewElement`] machinery as any other view. +#[derive(Clone, Debug)] +pub struct AnyView { + entity: AnyEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, +} + +impl From> for AnyView { + fn from(value: Entity) -> Self { + AnyView { + entity: value.into_any(), + render: any_view::render::, + } + } +} + +impl AnyView { + /// Embed this view as a cached [`ViewElement`] laid out at `style`. + /// + /// The rendered subtree is recycled from the previous frame unless + /// [Context::notify] was called on the backing entity since it was rendered + /// (or [Window::refresh] is called, which ignores caching). + pub fn cached(self, style: StyleRefinement) -> ViewElement { + ViewElement::new(self).cached(style) + } + + /// Convert this to a weak handle. + pub fn downgrade(&self) -> AnyWeakView { + AnyWeakView { + entity: self.entity.downgrade(), + render: self.render, + } + } + + /// Convert this to a [Entity] of a specific type. + /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant. + pub fn downcast(self) -> Result, Self> { + match self.entity.downcast() { + Ok(entity) => Ok(entity), + Err(entity) => Err(Self { + entity, + render: self.render, + }), + } + } + + /// Gets the [TypeId] of the underlying view. + pub fn entity_type(&self) -> TypeId { + self.entity.entity_type + } + + /// The [`EntityId`] of this view. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } +} + +impl PartialEq for AnyView { + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } +} + +impl Eq for AnyView {} + +/// `AnyView` is the type-erased [`View`]: its `render` is a function pointer rather +/// than a concrete type, but it participates in the reactive graph exactly like any +/// other view via [`ViewElement`]. +impl View for AnyView { + fn entity_id(&self) -> Option { + Some(self.entity.entity_id()) + } + + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + (self.render)(&self, window, cx) + } +} + +impl IntoElement for Entity { + type Element = ViewElement>; + + fn into_element(self) -> Self::Element { + ViewElement::new(self) + } + + #[inline(never)] + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } +} + +impl IntoElement for AnyView { + type Element = ViewElement; + + fn into_element(self) -> Self::Element { + ViewElement::new(self) + } +} + +/// A weak, dynamically-typed view handle. +pub struct AnyWeakView { + entity: AnyWeakEntity, + render: fn(&AnyView, &mut Window, &mut App) -> AnyElement, +} + +impl AnyWeakView { + /// Upgrade to a strong `AnyView` handle, if the view is still alive. + pub fn upgrade(&self) -> Option { + let entity = self.entity.upgrade()?; + Some(AnyView { + entity, + render: self.render, + }) + } +} + +impl From> for AnyWeakView { + fn from(view: WeakEntity) -> Self { + AnyWeakView { + entity: view.into(), + render: any_view::render::, + } + } +} + +impl PartialEq for AnyWeakView { + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } +} + +impl std::fmt::Debug for AnyWeakView { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AnyWeakView") + .field("entity_id", &self.entity.entity_id) + .finish_non_exhaustive() + } +} + +mod any_view { + use crate::{AnyElement, AnyView, App, IntoElement, Render, Window}; + + pub(crate) fn render( + view: &AnyView, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let view = view.clone().downcast::().unwrap(); + // Record the view's Render type name so the accessibility debug dump can + // attribute nodes to the view that produced them. + #[cfg(debug_assertions)] + window + .a11y + .view_type_names + .insert(view.entity_id(), std::any::type_name::()); + view.update(cx, |view, cx| view.render(window, cx).into_any_element()) + } +} + +/// A renderable that participates in GPUI's reactive graph — the unifying model +/// behind [`Render`] and [`RenderOnce`]. +/// +/// When `entity_id()` returns `Some`, that id becomes the view's identity: it gets +/// a unique element-id space (so internal `use_state` / `.id(..)` never collide +/// across siblings) and `cx.notify()` on that entity re-renders only this view's +/// subtree. `None` behaves like a stateless component. +/// +/// You rarely implement `View` directly. `Entity` and any `T: RenderOnce` +/// get a blanket impl below; implement it by hand only when a component needs both +/// parent-supplied props *and* a backing entity for identity. +pub trait View: 'static + Sized { + /// This view's identity, if it has one. A view typically holds the backing + /// entity as a field and returns its [`EntityId`] here. + /// + /// The id becomes this view's [`ElementId`], so two views keyed on the same + /// entity must not be rendered at the same position in the element tree + /// (e.g. as siblings under the same parent): their internal element state + /// (`use_state`, scroll offsets, etc.) would silently collide. Nesting is + /// fine — the id is scoped by the parent path. + fn entity_id(&self) -> Option; + + /// Render this view into an element tree, consuming `self`. + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement; +} + +/// A stateless component (`RenderOnce`) is a `View` with no identity. +impl View for T { + fn entity_id(&self) -> Option { + None + } + + #[inline] + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + RenderOnce::render(self, window, cx) + } +} + +/// An entity that renders itself (`Render`) is a `View` keyed on its own id. +impl View for Entity { + fn entity_id(&self) -> Option { + Some(Entity::entity_id(self)) + } + + #[inline] + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + self.update(cx, |this, cx| { + Render::render(this, window, cx).into_any_element() + }) + } +} + +impl Entity { + /// Embed this entity as a cached [`ViewElement`] laid out at `style`. + /// + /// The rendered subtree is reused until the entity is notified (or the + /// cached bounds / text style change). Caching requires a definite size: + /// a cached view is laid out from `style` and is *not* measured from its + /// contents. Use [`ViewElement::new`] (or `.child(entity)`) for the + /// uncached case. + #[track_caller] + pub fn cached(self, style: StyleRefinement) -> ViewElement> { + ViewElement::new(self).cached(style) + } +} + +/// The element type for [`View`] implementations. Wraps a `View` and hooks it +/// into layout, prepaint, and paint. Constructed via [`ViewElement::new`]. +#[doc(hidden)] +pub struct ViewElement { + view: Option, + entity_id: Option, + cached_style: Option, + #[cfg(debug_assertions)] + source: &'static core::panic::Location<'static>, +} + +impl ViewElement { + /// Wrap a [`View`] as an element. + #[track_caller] + pub fn new(view: V) -> Self { + let entity_id = view.entity_id(); + ViewElement { + entity_id, + cached_style: None, + view: Some(view), + #[cfg(debug_assertions)] + source: core::panic::Location::caller(), + } + } + + /// Enable caching of this view's rendered subtree, laid out at `style`. + /// The composer supplies the layout style because caching skips rendering + /// the contents to measure them. + /// + /// Crate-private on purpose: caching is only sound for entity-backed views, + /// where [`Context::notify`] is the contract that busts the cache. A stateless + /// view has no such contract, so a frozen subtree could never be invalidated. + /// Reach this through [`Entity::cached`] or [`AnyView::cached`], which are + /// entity-backed by construction. + pub(crate) fn cached(mut self, style: StyleRefinement) -> Self { + self.cached_style = Some(style); + self + } +} + +impl IntoElement for ViewElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +struct ViewElementState { + prepaint_range: Range, + paint_range: Range, + cache_key: ViewElementCacheKey, + accessed_entities: FxHashSet, +} + +struct ViewElementCacheKey { + bounds: Bounds, + content_mask: ContentMask, + text_style: TextStyle, +} + +impl Element for ViewElement { + type RequestLayoutState = Option; + type PrepaintState = Option; + + fn id(&self) -> Option { + self.entity_id.map(ElementId::View) + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + #[cfg(debug_assertions)] + return Some(self.source); + + #[cfg(not(debug_assertions))] + return None; + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + if let Some(entity_id) = self.entity_id { + // Stateful path: create a reactive boundary. + let view = &mut self.view; + request_layout_view( + entity_id, + self.cached_style.as_ref(), + window, + cx, + &mut |window, cx| view.take().unwrap().render(window, cx).into_any_element(), + ) + } else { + // Stateless path: isolate subtree via type name (no entity identity). + request_layout_component(type_name::(), window, cx, &mut |window, cx| { + self.view + .take() + .unwrap() + .render(window, cx) + .into_any_element() + }) + } + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + element: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Option { + if let Some(entity_id) = self.entity_id { + // Stateful path. + prepaint_view( + entity_id, + global_id, + bounds, + element, + window, + cx, + &mut |window, cx| { + self.view + .take() + .unwrap() + .render(window, cx) + .into_any_element() + }, + ) + } else { + // Stateless path: just prepaint the element. + prepaint_component(type_name::(), element, window, cx) + } + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + if let Some(entity_id) = self.entity_id { + // Stateful path. + paint_view( + entity_id, + self.cached_style.is_some(), + global_id, + element, + window, + cx, + ); + } else { + // Stateless path: just paint the element. + paint_component(std::any::type_name::(), element, window, cx); + } + } +} + +/// A view that renders nothing +pub struct EmptyView; + +impl Render for EmptyView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Empty + } +} + +#[inline(never)] +fn request_layout_view( + entity_id: EntityId, + cached_style: Option<&StyleRefinement>, + window: &mut Window, + cx: &mut App, + render: &mut dyn FnMut(&mut Window, &mut App) -> AnyElement, +) -> (LayoutId, Option) { + window.with_rendered_view(entity_id, |window| { + let caching_disabled = window.is_inspector_picking(cx); + match cached_style { + Some(style) if !caching_disabled => { + let mut root_style = Style::default(); + root_style.refine(style); + let layout_id = window.request_layout(root_style, None, cx); + (layout_id, None) + } + _ => { + let mut element = render(window, cx); + let layout_id = element.request_layout(window, cx); + (layout_id, Some(element)) + } + } + }) +} + +#[inline(never)] +fn request_layout_component( + name: &'static str, + window: &mut Window, + cx: &mut App, + render: &mut dyn FnMut(&mut Window, &mut App) -> AnyElement, +) -> (LayoutId, Option) { + window.with_id(ElementId::from(name), |window| { + let mut element = render(window, cx); + let layout_id = element.request_layout(window, cx); + (layout_id, Some(element)) + }) +} + +#[inline(never)] +fn prepaint_view( + entity_id: EntityId, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + element: &mut Option, + window: &mut Window, + cx: &mut App, + render: &mut dyn FnMut(&mut Window, &mut App) -> AnyElement, +) -> Option { + window.set_view_id(entity_id); + window.with_rendered_view(entity_id, |window| { + if let Some(mut element) = element.take() { + element.prepaint(window, cx); + return Some(element); + } + + window.with_element_state::( + global_id.unwrap(), + |element_state, window| { + let content_mask = window.content_mask(); + let text_style = window.text_style(); + + if let Some(mut element_state) = element_state + && element_state.cache_key.bounds == bounds + && element_state.cache_key.content_mask == content_mask + && element_state.cache_key.text_style == text_style + && !window.dirty_views.contains(&entity_id) + && !window.refreshing + { + let prepaint_start = window.prepaint_index(); + window.reuse_prepaint(element_state.prepaint_range.clone()); + cx.entities + .extend_accessed(&element_state.accessed_entities); + let prepaint_end = window.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; + + return (None, element_state); + } + + let refreshing = mem::replace(&mut window.refreshing, true); + let prepaint_start = window.prepaint_index(); + let (element, accessed_entities) = cx.detect_accessed_entities(|cx| { + let mut element = render(window, cx); + element.layout_as_root(Size::::from(bounds.size), window, cx); + element.prepaint_at(bounds.origin, window, cx); + element + }); + + let prepaint_end = window.prepaint_index(); + window.refreshing = refreshing; + + ( + Some(element), + ViewElementState { + accessed_entities, + prepaint_range: prepaint_start..prepaint_end, + paint_range: PaintIndex::default()..PaintIndex::default(), + cache_key: ViewElementCacheKey { + bounds, + content_mask, + text_style, + }, + }, + ) + }, + ) + }) +} + +#[inline(never)] +fn prepaint_component( + name: &'static str, + element: &mut Option, + window: &mut Window, + cx: &mut App, +) -> Option { + window.with_id(ElementId::from(name), |window| { + element.as_mut().unwrap().prepaint(window, cx); + }); + Some(element.take().unwrap()) +} + +#[inline(never)] +fn paint_view( + entity_id: EntityId, + cached: bool, + global_id: Option<&GlobalElementId>, + element: &mut Option, + window: &mut Window, + cx: &mut App, +) { + window.with_rendered_view(entity_id, |window| { + let caching_disabled = window.is_inspector_picking(cx); + if cached && !caching_disabled { + window.with_element_state::( + global_id.unwrap(), + |element_state, window| { + let mut element_state = element_state.unwrap(); + + let paint_start = window.paint_index(); + + if let Some(element) = element { + let refreshing = mem::replace(&mut window.refreshing, true); + element.paint(window, cx); + window.refreshing = refreshing; + } else { + window.reuse_paint(element_state.paint_range.clone()); + } + + let paint_end = window.paint_index(); + element_state.paint_range = paint_start..paint_end; + + ((), element_state) + }, + ) + } else { + element.as_mut().unwrap().paint(window, cx); + } + }); +} + +#[inline(never)] +fn paint_component( + name: &'static str, + element: &mut Option, + window: &mut Window, + cx: &mut App, +) { + window.with_id(ElementId::Name(name.into()), |window| { + element.as_mut().unwrap().paint(window, cx); + }); +} diff --git a/vendor/gpui-pre/src/window.rs b/vendor/gpui-pre/src/window.rs new file mode 100644 index 0000000..259a451 --- /dev/null +++ b/vendor/gpui-pre/src/window.rs @@ -0,0 +1,9528 @@ +#[cfg(feature = "profiler")] +use crate::DebugFrameOverlayMode; +#[cfg(any(feature = "inspector", debug_assertions))] +use crate::Inspector; +#[cfg(feature = "profiler")] +use crate::profiler; +use crate::{ + Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset, + AsyncWindowContext, AtlasTile, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, + Capslock, Context, Corners, CursorHideMode, CursorStyle, Decorations, DevicePixels, + DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, + EntityId, EventEmitter, FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, + Hsla, InputHandler, InputPreference, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, + Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers, ModifiersChangedEvent, + MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, Priority, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, + RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, + SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size, + StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, + SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task, TextInputConfiguration, + TextInputStateChange, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, + TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, + WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + WindowVisibility, point, prelude::*, px, rems, size, transparent_black, +}; + +use crate::gestures::{GestureTuning, RecognizedTouchGesture, TouchGestureRecognizer}; +use crate::interactive::TouchEvent; +use anyhow::{Context as _, Result, anyhow}; +use collections::{FxHashMap, FxHashSet}; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use core_video::pixel_buffer::CVPixelBuffer; +use derive_more::{Deref, DerefMut}; +use futures::channel::oneshot; +use gpui_util::post_inc; +use gpui_util::{ResultExt, measure}; +use itertools::FoldWhile::{Continue, Done}; +use itertools::Itertools; +use parking_lot::RwLock; +use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle}; +use refineable::Refineable; +use scheduler::Instant; +use slotmap::SlotMap; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + cell::{Cell, RefCell}, + cmp, + fmt::{Debug, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + ops::{DerefMut, Range}, + rc::Rc, + sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, + }, + thread::AccessError, + time::Duration, +}; +use uuid::Uuid; + +pub(crate) mod a11y; +mod prompts; + +pub use a11y::A11ySubtreeBuilder; + +use self::a11y::A11y; +#[cfg(not(target_family = "wasm"))] +use self::a11y::ROOT_NODE_ID; +use crate::util::{ + atomic_incr_if_not_zero, ceil_to_device_pixel, floor_to_device_pixel, round_half_toward_zero, + round_half_toward_zero_f64, round_stroke_to_device_pixel, round_to_device_pixel, +}; +pub use prompts::*; + +/// Default window size used when no explicit size is provided. +pub const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(1095.)); + +/// A 6:5 aspect ratio minimum window size to be used for functional, +/// additional-to-main-Zed windows, like the settings and rules library windows. +pub const DEFAULT_ADDITIONAL_WINDOW_SIZE: Size = Size { + width: Pixels(900.), + height: Pixels(750.), +}; + +/// Represents the two different phases when dispatching events. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub enum DispatchPhase { + /// After the capture phase comes the bubble phase, in which mouse event listeners are + /// invoked front to back and keyboard event listeners are invoked from the focused element + /// to the root of the element tree. This is the phase you'll most commonly want to use when + /// registering event listeners. + #[default] + Bubble, + /// During the initial capture phase, mouse event listeners are invoked back to front, and keyboard + /// listeners are invoked from the root of the tree downward toward the focused element. This phase + /// is used for special purposes such as clearing the "pressed" state for click events. If + /// you stop event propagation during this phase, you need to know what you're doing. Handlers + /// outside of the immediate region may rely on detecting non-local events during this phase. + Capture, +} + +impl DispatchPhase { + /// Returns true if this represents the "bubble" phase. + #[inline] + pub fn bubble(self) -> bool { + self == DispatchPhase::Bubble + } + + /// Returns true if this represents the "capture" phase. + #[inline] + pub fn capture(self) -> bool { + self == DispatchPhase::Capture + } +} + +struct WindowInvalidatorInner { + #[cfg(feature = "profiler")] + pub window_id: WindowId, + pub dirty: bool, + pub draw_phase: DrawPhase, + pub dirty_views: FxHashSet, + pub update_count: usize, + #[cfg(feature = "profiler")] + pub frame_dirty: FrameDirtyAccumulator, + pub platform_waker: Option>, +} + +/// Per-frame invalidation bookkeeping, drained at draw time and emitted to the +/// frame profiler. Tracks when the current frame first became dirty and how +/// many invalidations were coalesced into it, whenever the profiler is +/// compiled in. Retention of the resulting per-frame records is what +/// `profiler::trace_enabled()` controls, not this measurement. +#[cfg(feature = "profiler")] +#[derive(Default)] +struct FrameDirtyAccumulator { + dirty_at: Option, + invalidations: u64, +} + +/// Why GPUI capped a window's frame rate. +#[derive(Clone, Copy)] +enum FrameRateLimit { + /// The window isn't focused. + Inactive, + /// The system is under thermal pressure. + Thermal, +} + +#[derive(Clone)] +pub(crate) struct WindowInvalidator { + inner: Rc>, +} + +impl WindowInvalidator { + pub fn new(#[allow(unused_variables)] window_id: WindowId) -> Self { + WindowInvalidator { + inner: Rc::new(RefCell::new(WindowInvalidatorInner { + #[cfg(feature = "profiler")] + window_id, + dirty: true, + draw_phase: DrawPhase::None, + dirty_views: FxHashSet::default(), + update_count: 0, + #[cfg(feature = "profiler")] + frame_dirty: FrameDirtyAccumulator::default(), + platform_waker: None, + })), + } + } + + pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool { + let mut inner = self.inner.borrow_mut(); + inner.update_count += 1; + inner.dirty_views.insert(entity); + if inner.draw_phase == DrawPhase::None { + #[cfg(feature = "profiler")] + let dirty_at = Self::record_frame_dirty(&mut inner); + let became_dirty = !inner.dirty; + inner.dirty = true; + let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); + #[cfg(feature = "profiler")] + let window_id = inner.window_id; + drop(inner); + #[cfg(feature = "profiler")] + if became_dirty { + profiler::journal::record_frame_pending(window_id, dirty_at); + } + cx.push_effect(Effect::Notify { emitter: entity }); + if let Some(waker) = waker { + waker(); + } + true + } else { + false + } + } + + pub fn is_dirty(&self) -> bool { + self.inner.borrow().dirty + } + + pub fn set_dirty(&self, dirty: bool) { + let mut inner = self.inner.borrow_mut(); + let became_dirty = dirty && !inner.dirty; + inner.dirty = dirty; + if dirty { + inner.update_count += 1; + } + #[cfg(feature = "profiler")] + let dirty_at = dirty.then(|| Self::record_frame_dirty(&mut inner)); + let waker = became_dirty.then(|| inner.platform_waker.clone()).flatten(); + #[cfg(feature = "profiler")] + let window_id = inner.window_id; + drop(inner); + #[cfg(feature = "profiler")] + if became_dirty && let Some(dirty_at) = dirty_at { + profiler::journal::record_frame_pending(window_id, dirty_at); + } + if let Some(waker) = waker { + waker(); + } + } + + pub fn set_platform_waker(&self, waker: Option>) { + let mut inner = self.inner.borrow_mut(); + inner.platform_waker = waker; + let waker = inner.dirty.then(|| inner.platform_waker.clone()).flatten(); + drop(inner); + if let Some(waker) = waker { + waker(); + } + } + + /// Wakes the platform's frame-request source so a frame request is + /// delivered even if the platform stops requesting frames for idle + /// windows. No-op on platforms without a frame waker. + pub fn wake_platform(&self) { + let waker = self.inner.borrow().platform_waker.clone(); + if let Some(waker) = waker { + waker(); + } + } + + pub fn set_phase(&self, phase: DrawPhase) { + self.inner.borrow_mut().draw_phase = phase + } + + pub fn update_count(&self) -> usize { + self.inner.borrow().update_count + } + + #[cfg(feature = "profiler")] + fn record_frame_dirty(inner: &mut WindowInvalidatorInner) -> Instant { + let dirty_at = *inner.frame_dirty.dirty_at.get_or_insert_with(Instant::now); + inner.frame_dirty.invalidations += 1; + dirty_at + } + + #[cfg(feature = "profiler")] + fn take_frame_dirty(&self) -> FrameDirtyAccumulator { + mem::take(&mut self.inner.borrow_mut().frame_dirty) + } + + pub fn take_views(&self) -> FxHashSet { + mem::take(&mut self.inner.borrow_mut().dirty_views) + } + + pub fn replace_views(&self, views: FxHashSet) { + self.inner.borrow_mut().dirty_views = views; + } + + pub fn not_drawing(&self) -> bool { + self.inner.borrow().draw_phase == DrawPhase::None + } + + #[track_caller] + pub fn debug_assert_paint(&self) { + debug_assert!( + matches!(self.inner.borrow().draw_phase, DrawPhase::Paint), + "this method can only be called during paint" + ); + } + + #[track_caller] + pub fn debug_assert_prepaint(&self) { + debug_assert!( + matches!(self.inner.borrow().draw_phase, DrawPhase::Prepaint), + "this method can only be called during request_layout, or prepaint" + ); + } + + #[track_caller] + pub fn debug_assert_paint_or_prepaint(&self) { + debug_assert!( + matches!( + self.inner.borrow().draw_phase, + DrawPhase::Paint | DrawPhase::Prepaint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + } +} + +type AnyObserver = Box bool + 'static>; + +pub(crate) type AnyWindowFocusListener = + Box bool + 'static>; + +pub(crate) struct WindowFocusEvent { + pub(crate) previous_focus_path: SmallVec<[FocusId; 8]>, + pub(crate) current_focus_path: SmallVec<[FocusId; 8]>, +} + +impl WindowFocusEvent { + pub fn is_focus_in(&self, focus_id: FocusId) -> bool { + !self.previous_focus_path.contains(&focus_id) && self.current_focus_path.contains(&focus_id) + } + + pub fn is_focus_out(&self, focus_id: FocusId) -> bool { + self.previous_focus_path.contains(&focus_id) && !self.current_focus_path.contains(&focus_id) + } +} + +/// This is provided when subscribing for `Context::on_focus_out` events. +pub struct FocusOutEvent { + /// A weak focus handle representing what was blurred. + pub blurred: WeakFocusHandle, +} + +slotmap::new_key_type! { + /// A globally unique identifier for a focusable element. + pub struct FocusId; +} + +thread_local! { + /// Fallback arena used when no app-specific arena is active. + /// In production, each window draw sets CURRENT_ELEMENT_ARENA to the app's arena. + pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(1024 * 1024)); + + /// Points to the current App's element arena during draw operations. + /// This allows multiple test Apps to have isolated arenas, preventing + /// cross-session corruption when the scheduler interleaves their tasks. + static CURRENT_ELEMENT_ARENA: Cell>> = const { Cell::new(None) }; +} + +/// Whether a window draw is currently in progress on this thread. +/// +/// This holds exactly while an `ElementArenaScope` is active: nested scopes +/// restore the previous (still set) arena pointer, so `CURRENT_ELEMENT_ARENA` +/// is `Some` from the outermost draw's start to its end. +/// +/// The `on_request_frame` callback uses this to defer draw requests that +/// arrive re-entrantly while a draw is already on the stack (e.g. via nested +/// message pumping in the Windows window procedure), instead of running a +/// nested draw or panicking on the already-borrowed App. +fn draw_in_progress() -> bool { + CURRENT_ELEMENT_ARENA.with(|current| current.get().is_some()) +} + +/// Allocates an element in the current arena. Uses the app-specific arena if one +/// is active (during draw), otherwise falls back to the thread-local ELEMENT_ARENA. +#[inline(always)] +pub(crate) fn with_element_arena(callback: impl FnOnce(&mut Arena) -> R) -> R { + let mut callback = Some(callback); + let mut result = None; + let access = with_element_arena_erased(&mut |arena| { + result = Some(callback.take().expect("arena callback runs once")(arena)); + }); + drop(callback); + access.expect("cannot access a Thread Local Storage value during or after destruction"); + result.expect("arena callback produces a result") +} + +/// Scope guard that sets CURRENT_ELEMENT_ARENA for the duration of a draw +/// operation and tracks the arena's scope depth, so that a nested draw's +/// `ArenaClearNeeded::clear` is deferred rather than freeing memory the outer +/// draw still references (see `Arena::clear`). +/// +/// Call [`ElementArenaScope::exit`] with the same arena that was entered to +/// obtain the [`ArenaClearNeeded`] token the draw now owes; requiring `exit` +/// makes it impossible to request a clear before the scope has ended. The +/// scope's teardown — restoring the thread-local and balancing `begin_scope` +/// with `end_scope` — happens in `Drop`, so the arena's scope depth stays +/// balanced on every path, including when a panic unwinds a draw before `exit` +/// is reached. (If teardown lived only in `exit`, such a panic would leave the +/// scope depth permanently elevated and defer every future clear, leaking +/// memory unboundedly.) +pub(crate) struct ElementArenaScope { + /// The entered arena: compared against the argument in `exit`, and + /// dereferenced in `Drop` to end its scope (see the SAFETY note there). + entered: *const RefCell, + previous: Option<*const RefCell>, + exited: bool, +} + +impl ElementArenaScope { + /// Enter a scope where element allocations use the given arena. + pub(crate) fn enter(arena: &RefCell) -> Self { + arena.borrow_mut().begin_scope(); + let previous = CURRENT_ELEMENT_ARENA.with(|current| { + let prev = current.get(); + current.set(Some(arena as *const RefCell)); + prev + }); + Self { + entered: arena as *const RefCell, + previous, + exited: false, + } + } + + /// End the scope: restores the previously-current arena and ends the + /// arena's clear-deferral scope. Returns the token for the arena clear the + /// draw now owes; producing it here makes it impossible to request a clear + /// before the scope has ended (which would be silently deferred forever). + /// + /// Panics if passed a different arena than was entered: ending the scope + /// of the wrong arena would unbalance two arenas' scope depths, allowing + /// one of them to clear while a draw still references its memory. + pub(crate) fn exit(mut self, arena: &RefCell) -> ArenaClearNeeded { + assert!( + std::ptr::eq(self.entered, arena), + "ElementArenaScope::exit called with a different arena than was entered" + ); + self.exited = true; + // Teardown (restoring the thread-local and ending the arena's + // clear-deferral scope) runs in `Drop`, which fires both here — `self` + // is dropped as `exit` returns, before the token reaches the caller — + // and when a panic unwinds the draw before `exit` is reached. + ArenaClearNeeded::new(arena) + } +} + +impl Drop for ElementArenaScope { + fn drop(&mut self) { + // Teardown lives here (rather than in `exit`) so it runs exactly once on + // every path: `exit` consumes and drops the guard on the normal path, + // and unwinding drops it on the panic path. Balancing `begin_scope` here + // keeps the arena's scope depth correct even when a draw panics; if this + // only happened in `exit`, a panic between `enter` and `exit` would leave + // the depth elevated and defer every future clear. + CURRENT_ELEMENT_ARENA.with(|current| { + current.set(self.previous); + }); + // SAFETY: `entered` came from a `&RefCell` in `enter`, and the + // arena (owned by the `App` being drawn) outlives this guard on both the + // normal and unwinding paths, since the guard is a local of the draw. + unsafe { &*self.entered }.borrow_mut().end_scope(); + if !self.exited && !std::thread::panicking() { + debug_assert!(false, "ElementArenaScope dropped without calling exit()"); + log::error!( + "ElementArenaScope dropped without calling exit(); \ + the arena clear for this draw was never requested" + ); + } + } +} + +/// Returned when the element arena has been used and so must be cleared before the next draw. +#[must_use] +pub struct ArenaClearNeeded { + /// Identity of the arena that was drawn into. Only ever compared against + /// another pointer in `clear`; never dereferenced. + arena: *const RefCell, +} + +impl ArenaClearNeeded { + /// Create a new ArenaClearNeeded token for the App whose arena was drawn + /// into. Private: the only way to obtain one is [`ElementArenaScope::exit`]. + fn new(arena: &RefCell) -> Self { + Self { + arena: arena as *const RefCell, + } + } + + /// Clear the element arena of the App the draw ran against. If an enclosing + /// draw is still in progress (this draw was nested inside it), the clear is + /// deferred to the enclosing draw's own `ArenaClearNeeded` so that its live + /// allocations aren't freed. + /// + /// Panics if passed a different App than the draw ran against, since + /// clearing another App's arena could free memory its draws still + /// reference. + pub fn clear(self, cx: &mut App) { + assert!( + std::ptr::eq(self.arena, &cx.element_arena), + "ArenaClearNeeded::clear called with a different App than the draw ran against" + ); + cx.element_arena.borrow_mut().clear(); + } +} + +pub(crate) type FocusMap = RwLock>; +pub(crate) struct FocusRef { + pub(crate) ref_count: AtomicUsize, + pub(crate) tab_index: isize, + pub(crate) tab_stop: bool, +} + +impl FocusId { + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, window: &Window) -> bool { + window.focus == Some(*self) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { + window + .focused(cx) + .is_some_and(|focused| self.contains(focused.id, window)) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, window: &Window, cx: &App) -> bool { + let focused = window.focused(cx); + focused.is_some_and(|focused| focused.id.contains(*self, window)) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub(crate) fn contains(&self, other: Self, window: &Window) -> bool { + window + .rendered_frame + .dispatch_tree + .focus_contains(*self, other) + } +} + +/// A handle which can be used to track and manipulate the focused element in a window. +pub struct FocusHandle { + pub(crate) id: FocusId, + handles: Arc, + /// The index of this element in the tab order. + pub tab_index: isize, + /// Whether this element can be focused by tab navigation. + pub tab_stop: bool, +} + +impl std::fmt::Debug for FocusHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("FocusHandle({:?})", self.id)) + } +} + +impl FocusHandle { + pub(crate) fn new(handles: &Arc) -> Self { + let id = handles.write().insert(FocusRef { + ref_count: AtomicUsize::new(1), + tab_index: 0, + tab_stop: false, + }); + + Self { + id, + tab_index: 0, + tab_stop: false, + handles: handles.clone(), + } + } + + pub(crate) fn for_id(id: FocusId, handles: &Arc) -> Option { + let lock = handles.read(); + let focus = lock.get(id)?; + if atomic_incr_if_not_zero(&focus.ref_count) == 0 { + return None; + } + Some(Self { + id, + tab_index: focus.tab_index, + tab_stop: focus.tab_stop, + handles: handles.clone(), + }) + } + + /// Sets the tab index of the element associated with this handle. + pub fn tab_index(mut self, index: isize) -> Self { + self.tab_index = index; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_index = index; + } + self + } + + /// Sets whether the element associated with this handle is a tab stop. + /// + /// When `false`, the element will not be included in the tab order. + pub fn tab_stop(mut self, tab_stop: bool) -> Self { + self.tab_stop = tab_stop; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_stop = tab_stop; + } + self + } + + /// Converts this focus handle into a weak variant, which does not prevent it from being released. + pub fn downgrade(&self) -> WeakFocusHandle { + WeakFocusHandle { + id: self.id, + handles: Arc::downgrade(&self.handles), + } + } + + /// Moves the focus to the element associated with this handle. + pub fn focus(&self, window: &mut Window, cx: &mut App) { + window.focus(self, cx) + } + + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, window: &Window) -> bool { + self.id.is_focused(window) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { + self.id.contains_focused(window, cx) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, window: &Window, cx: &mut App) -> bool { + self.id.within_focused(window, cx) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub fn contains(&self, other: &Self, window: &Window) -> bool { + self.id.contains(other.id, window) + } + + /// Dispatch an action on the element that rendered this focus handle + pub fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) { + if let Some(node_id) = window + .rendered_frame + .dispatch_tree + .focusable_node_id(self.id) + { + window.dispatch_action_on_node(node_id, action, cx) + } + } +} + +impl Clone for FocusHandle { + fn clone(&self) -> Self { + Self::for_id(self.id, &self.handles).unwrap() + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for FocusHandle {} + +impl Drop for FocusHandle { + fn drop(&mut self) { + self.handles + .read() + .get(self.id) + .unwrap() + .ref_count + .fetch_sub(1, SeqCst); + } +} + +/// A weak reference to a focus handle. +#[derive(Clone, Debug)] +pub struct WeakFocusHandle { + pub(crate) id: FocusId, + pub(crate) handles: Weak, +} + +impl WeakFocusHandle { + /// Attempts to upgrade the [WeakFocusHandle] to a [FocusHandle]. + pub fn upgrade(&self) -> Option { + let handles = self.handles.upgrade()?; + FocusHandle::for_id(self.id, &handles) + } +} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +impl Eq for WeakFocusHandle {} + +impl PartialEq for WeakFocusHandle { + fn eq(&self, other: &FocusHandle) -> bool { + self.id == other.id + } +} + +impl PartialEq for FocusHandle { + fn eq(&self, other: &WeakFocusHandle) -> bool { + self.id == other.id + } +} + +/// Focusable allows users of your view to easily +/// focus it (using window.focus_view(cx, view)) +pub trait Focusable: 'static { + /// Returns the focus handle associated with this view. + fn focus_handle(&self, cx: &App) -> FocusHandle; +} + +impl Focusable for Entity { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.read(cx).focus_handle(cx) + } +} + +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: Focusable + EventEmitter + Render {} + +impl + Render> ManagedView for M {} + +/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. +pub struct DismissEvent; + +type FrameCallback = Box; + +pub(crate) type AnyMouseListener = + Box; + +#[derive(Clone)] +pub(crate) struct CursorStyleRequest { + pub(crate) hitbox_id: Option, + pub(crate) style: CursorStyle, +} + +#[derive(Default, Eq, PartialEq)] +pub(crate) struct HitTest { + pub(crate) ids: SmallVec<[HitboxId; 8]>, + pub(crate) hover_hitbox_count: usize, +} + +/// A type of window control area that corresponds to the platform window. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WindowControlArea { + /// An area that allows dragging of the platform window. + Drag, + /// An area that allows closing of the platform window. + Close, + /// An area that allows maximizing of the platform window. + Max, + /// An area that allows minimizing of the platform window. + Min, +} + +/// An identifier for a [Hitbox] which also includes [HitboxBehavior]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct HitboxId(u64); + +#[cfg(feature = "test-support")] +impl HitboxId { + /// A placeholder HitboxId exclusively for integration testing API's that + /// need a hitbox but where the value of the hitbox does not matter. The + /// alternative is to make the Hitbox optional but that complicates the + /// implementation. + pub const fn placeholder() -> Self { + Self(0) + } +} + +impl HitboxId { + /// Checks if the hitbox with this ID is currently hovered. Returns `false` during keyboard + /// input modality so that keyboard navigation suppresses hover highlights. Except when handling + /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse + /// events or paint hover styles. + /// + /// See [`Hitbox::is_hovered`] for details. + pub fn is_hovered(self, window: &Window) -> bool { + // If this hitbox has captured the pointer, it's always considered hovered + if window.captured_hitbox == Some(self) { + return true; + } + if window.last_input_was_keyboard() { + return false; + } + self.hit_test(window) + } + + /// Checks if the hitbox with this ID is currently hovered, regardless of the last + /// input modality used. + /// + /// See [`HitboxId::is_hovered`] for more details. + pub(crate) fn is_hovered_ignoring_last_input(self, window: &Window) -> bool { + // If this hitbox has captured the pointer, it's always considered hovered + if window.captured_hitbox == Some(self) { + return true; + } + self.hit_test(window) + } + + fn hit_test(self, window: &Window) -> bool { + let hit_test = &window.mouse_hit_test; + for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { + if self == *id { + return true; + } + } + false + } + + /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. + /// Typically this should only be used when handling `ScrollWheelEvent`, and otherwise + /// `is_hovered` should be used. See the documentation of `Hitbox::is_hovered` for details about + /// this distinction. + pub fn should_handle_scroll(self, window: &Window) -> bool { + window.mouse_hit_test.ids.contains(&self) + } + + fn next(mut self) -> HitboxId { + HitboxId(self.0.wrapping_add(1)) + } +} + +/// A rectangular region that potentially blocks hitboxes inserted prior. +/// See [Window::insert_hitbox] for more details. +#[derive(Clone, Debug, Deref)] +pub struct Hitbox { + /// A unique identifier for the hitbox. + pub id: HitboxId, + /// The bounds of the hitbox. + #[deref] + pub bounds: Bounds, + /// The content mask when the hitbox was inserted. + pub content_mask: ContentMask, + /// Flags that specify hitbox behavior. + pub behavior: HitboxBehavior, +} + +impl Hitbox { + /// Checks if the hitbox is currently hovered. Returns `false` during keyboard input modality + /// so that keyboard navigation suppresses hover highlights. Except when handling + /// `ScrollWheelEvent`, this is typically what you want when determining whether to handle mouse + /// events or paint hover styles. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`) or + /// `HitboxBehavior::BlockMouseExceptScroll` (`InteractiveElement::block_mouse_except_scroll`), + /// or if the current input modality is keyboard (see [`Window::last_input_was_keyboard`]). + /// + /// Handling of `ScrollWheelEvent` should typically use `should_handle_scroll` instead. + /// Concretely, this is due to use-cases like overlays that cause the elements under to be + /// non-interactive while still allowing scrolling. More abstractly, this is because + /// `is_hovered` is about element interactions directly under the mouse - mouse moves, clicks, + /// hover styling, etc. In contrast, scrolling is about finding the current outer scrollable + /// container. + pub fn is_hovered(&self, window: &Window) -> bool { + self.id.is_hovered(window) + } + + /// Checks whether this hitbox would be hovered at `position`, regardless of the current input + /// modality or mouse position. + pub fn is_hovered_at(&self, position: Point, window: &Window) -> bool { + let hit_test = window.rendered_frame.hit_test(position); + hit_test + .ids + .iter() + .take(hit_test.hover_hitbox_count) + .any(|id| self.id == *id) + } + + /// Checks if the hitbox contains the mouse and should handle scroll events. Typically this + /// should only be used when handling `ScrollWheelEvent`, and otherwise `is_hovered` should be + /// used. See the documentation of `Hitbox::is_hovered` for details about this distinction. + /// + /// This can return `false` even when the hitbox contains the mouse, if a hitbox in front of + /// this sets `HitboxBehavior::BlockMouse` (`InteractiveElement::occlude`). + pub fn should_handle_scroll(&self, window: &Window) -> bool { + self.id.should_handle_scroll(window) + } +} + +/// How the hitbox affects mouse behavior. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum HitboxBehavior { + /// Normal hitbox mouse behavior, doesn't affect mouse handling for other hitboxes. + #[default] + Normal, + + /// All hitboxes behind this hitbox will be ignored and so will have `hitbox.is_hovered() == + /// false` and `hitbox.should_handle_scroll() == false`. Typically for elements this causes + /// skipping of all mouse events, hover styles, and tooltips. This flag is set by + /// [`InteractiveElement::occlude`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type: + /// + /// ```ignore + /// window.on_mouse_event(move |_: &EveryMouseEventTypeHere, phase, window, cx| { + /// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) { + /// cx.stop_propagation(); + /// } + /// }) + /// ``` + /// + /// This has effects beyond event handling - any use of hitbox checking, such as hover + /// styles and tooltips. These other behaviors are the main point of this mechanism. An + /// alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouse, + + /// All hitboxes behind this hitbox will have `hitbox.is_hovered() == false`, even when + /// `hitbox.should_handle_scroll() == true`. Typically for elements this causes all mouse + /// interaction except scroll events to be ignored - see the documentation of + /// [`Hitbox::is_hovered`] for details. This flag is set by + /// [`InteractiveElement::block_mouse_except_scroll`]. + /// + /// For mouse handlers that check those hitboxes, this behaves the same as registering a + /// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`: + /// + /// ```ignore + /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| { + /// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) { + /// cx.stop_propagation(); + /// } + /// }) + /// ``` + /// + /// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is + /// handled differently than other mouse events. If also blocking these scroll events is + /// desired, then a `cx.stop_propagation()` handler like the one above can be used. + /// + /// This has effects beyond event handling - this affects any use of `is_hovered`, such as + /// hover styles and tooltips. These other behaviors are the main point of this mechanism. + /// An alternative might be to not affect mouse event handling - but this would allow + /// inconsistent UI where clicks and moves interact with elements that are not considered to + /// be hovered. + BlockMouseExceptScroll, +} + +/// An identifier for a tooltip. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct TooltipId(usize); + +impl TooltipId { + /// Checks if the tooltip is currently hovered. + pub fn is_hovered(&self, window: &Window) -> bool { + window + .tooltip_bounds + .as_ref() + .is_some_and(|tooltip_bounds| { + tooltip_bounds.id == *self + && tooltip_bounds.bounds.contains(&window.mouse_position()) + }) + } +} + +pub(crate) struct TooltipBounds { + id: TooltipId, + bounds: Bounds, +} + +#[derive(Clone)] +pub(crate) struct TooltipRequest { + id: TooltipId, + tooltip: AnyTooltip, +} + +pub(crate) struct DeferredDraw { + current_view: EntityId, + priority: usize, + parent_node: DispatchNodeId, + element_id_stack: SmallVec<[ElementId; 32]>, + text_style_stack: Vec, + content_mask: Option>, + rem_size: Pixels, + element: Option, + absolute_offset: Point, + prepaint_range: Range, + paint_range: Range, +} + +pub(crate) struct Frame { + pub(crate) focus: Option, + pub(crate) window_active: bool, + pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, + accessed_element_states: Vec<(GlobalElementId, TypeId)>, + pub(crate) mouse_listeners: Vec>, + pub(crate) dispatch_tree: DispatchTree, + pub(crate) scene: Scene, + pub(crate) hitboxes: Vec, + pub(crate) window_control_hitboxes: Vec<(WindowControlArea, Hitbox)>, + pub(crate) deferred_draws: Vec, + pub(crate) input_handlers: Vec>, + pub(crate) tooltip_requests: Vec>, + pub(crate) cursor_styles: Vec, + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: FxHashMap>, + #[cfg(any(test, feature = "test-support"))] + debug_bounds_records: Vec<(String, Bounds)>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) next_inspector_instance_ids: FxHashMap, usize>, + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) inspector_hitboxes: FxHashMap, + pub(crate) tab_stops: TabStopMap, +} + +#[derive(Clone, Default)] +pub(crate) struct PrepaintStateIndex { + hitboxes_index: usize, + tooltips_index: usize, + deferred_draws_index: usize, + dispatch_tree_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +#[derive(Clone, Default)] +pub(crate) struct PaintIndex { + scene_index: usize, + #[cfg(any(test, feature = "test-support"))] + debug_bounds_index: usize, + mouse_listeners_index: usize, + input_handlers_index: usize, + cursor_styles_index: usize, + accessed_element_states_index: usize, + tab_handle_index: usize, + line_layout_index: LineLayoutIndex, +} + +impl Frame { + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn record_debug_bounds(&mut self, selector: String, bounds: Bounds) { + self.debug_bounds.insert(selector.clone(), bounds); + self.debug_bounds_records.push((selector, bounds)); + } + + pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + focus: None, + window_active: false, + element_states: FxHashMap::default(), + accessed_element_states: Vec::new(), + mouse_listeners: Vec::new(), + dispatch_tree, + scene: Scene::default(), + hitboxes: Vec::new(), + window_control_hitboxes: Vec::new(), + deferred_draws: Vec::new(), + input_handlers: Vec::new(), + tooltip_requests: Vec::new(), + cursor_styles: Vec::new(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), + #[cfg(any(test, feature = "test-support"))] + debug_bounds_records: Vec::new(), + + #[cfg(any(feature = "inspector", debug_assertions))] + next_inspector_instance_ids: FxHashMap::default(), + + #[cfg(any(feature = "inspector", debug_assertions))] + inspector_hitboxes: FxHashMap::default(), + tab_stops: TabStopMap::default(), + } + } + + pub(crate) fn clear(&mut self) { + self.element_states.clear(); + self.accessed_element_states.clear(); + self.mouse_listeners.clear(); + self.dispatch_tree.clear(); + self.scene.clear(); + self.input_handlers.clear(); + self.tooltip_requests.clear(); + self.cursor_styles.clear(); + self.hitboxes.clear(); + self.window_control_hitboxes.clear(); + self.deferred_draws.clear(); + self.tab_stops.clear(); + self.focus = None; + + #[cfg(any(test, feature = "test-support"))] + { + self.debug_bounds.clear(); + self.debug_bounds_records.clear(); + } + + #[cfg(any(feature = "inspector", debug_assertions))] + { + self.next_inspector_instance_ids.clear(); + self.inspector_hitboxes.clear(); + } + } + + pub(crate) fn cursor_style(&self, window: &Window) -> Option { + self.cursor_styles + .iter() + .rev() + .fold_while(None, |style, request| match request.hitbox_id { + None => Done(Some(request.style)), + Some(hitbox_id) => Continue(style.or_else(|| { + hitbox_id + .is_hovered_ignoring_last_input(window) + .then_some(request.style) + })), + }) + .into_inner() + } + + pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut set_hover_hitbox_count = false; + let mut hit_test = HitTest::default(); + for hitbox in self.hitboxes.iter().rev() { + let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); + if bounds.contains(&position) { + hit_test.ids.push(hitbox.id); + if !set_hover_hitbox_count + && hitbox.behavior == HitboxBehavior::BlockMouseExceptScroll + { + hit_test.hover_hitbox_count = hit_test.ids.len(); + set_hover_hitbox_count = true; + } + if hitbox.behavior == HitboxBehavior::BlockMouse { + break; + } + } + } + if !set_hover_hitbox_count { + hit_test.hover_hitbox_count = hit_test.ids.len(); + } + hit_test + } + + pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { + self.focus + .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) + .unwrap_or_default() + } + + pub(crate) fn finish(&mut self, prev_frame: &mut Self) { + for element_state_key in &self.accessed_element_states { + if let Some((element_state_key, element_state)) = + prev_frame.element_states.remove_entry(element_state_key) + { + self.element_states.insert(element_state_key, element_state); + } + } + + self.scene.finish(); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +enum InputModality { + Mouse, + Keyboard, + Touch, +} + +/// Holds the state for a specific window. +pub struct Window { + pub(crate) handle: AnyWindowHandle, + pub(crate) invalidator: WindowInvalidator, + pub(crate) removed: bool, + pub(crate) platform_window: Box, + display_id: Option, + is_resizable: bool, + is_minimizable: bool, + sprite_atlas: Arc, + text_system: Arc, + text_rendering_mode: Rc>, + rem_size: Pixels, + /// The stack of override values for the window's rem size. + /// + /// This is used by `with_rem_size` to allow rendering an element tree with + /// a given rem size. + rem_size_override_stack: SmallVec<[Pixels; 8]>, + pub(crate) viewport_size: Size, + layout_engine: Option, + pub(crate) root: Option, + pub(crate) element_id_stack: SmallVec<[ElementId; 32]>, + pub(crate) text_style_stack: Vec, + pub(crate) rendered_entity_stack: Vec, + pub(crate) element_offset_stack: Vec>, + pub(crate) element_opacity: f32, + pub(crate) content_mask_stack: Vec>, + pub(crate) requested_autoscroll: Option>, + /// The [`TextInputConfiguration`] most recently forwarded to the platform + /// window, so that only actual changes are forwarded (reconfiguring a live + /// input session can restart the IME connection). + last_text_input_configuration: Option, + focused_text_input_active: bool, + pub(crate) image_cache_stack: Vec, + pub(crate) rendered_frame: Frame, + pub(crate) next_frame: Frame, + next_hitbox_id: HitboxId, + pub(crate) next_tooltip_id: TooltipId, + pub(crate) tooltip_bounds: Option, + pub(crate) next_frame_callbacks: Rc>>, + pub(crate) dirty_views: FxHashSet, + focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, + pub(crate) focus_lost_listeners: SubscriberSet<(), AnyObserver>, + focus_lost_path: SmallVec<[FocusId; 8]>, + default_prevented: bool, + mouse_position: Point, + mouse_hit_test: HitTest, + modifiers: Modifiers, + capslock: Capslock, + scale_factor: f32, + pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>, + appearance: WindowAppearance, + pub(crate) appearance_observers: SubscriberSet<(), AnyObserver>, + pub(crate) button_layout_observers: SubscriberSet<(), AnyObserver>, + active: Rc>, + visibility: WindowVisibility, + pub(crate) visibility_observers: + SubscriberSet<(), Box bool>>, + hovered: Rc>, + pub(crate) needs_present: Rc>, + /// Tracks recent input event timestamps to determine if input is arriving at a high rate. + /// Used to selectively enable VRR optimization only when input rate exceeds 60fps. + pub(crate) input_rate_tracker: Rc>, + #[cfg(feature = "profiler")] + window_profiler: profiler::WindowProfiler, + last_input_modality: InputModality, + touch_gestures: TouchGestureRecognizer, + touch_prediction_enabled: bool, + long_press_timer: Option>, + long_press_capture: Option, + pub(crate) refreshing: bool, + pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, + pub(crate) focus: Option, + focus_enabled: bool, + /// Incremented every time focus moves. Used to invalidate a + /// pending keyboard activation state when focus changes. + pub(crate) focus_generation: u64, + pending_input: Option, + pending_modifier: ModifierState, + pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, + prompt: Option, + pub(crate) client_inset: Option, + /// The hitbox that has captured the pointer, if any. + /// While captured, mouse events route to this hitbox regardless of hit testing. + captured_hitbox: Option, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector: Option>, + #[cfg(feature = "profiler")] + debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay, + pub(crate) a11y: A11y, +} + +#[derive(Clone, Debug, Default)] +struct ModifierState { + modifiers: Modifiers, + saw_other_input: bool, +} + +/// Tracks input event timestamps to determine if input is arriving at a high rate. +/// Used for selective VRR (Variable Refresh Rate) optimization. +#[derive(Clone, Debug)] +pub(crate) struct InputRateTracker { + timestamps: Vec, + window: Duration, + inputs_per_second: u32, + sustain_until: Instant, + sustain_duration: Duration, +} + +impl Default for InputRateTracker { + fn default() -> Self { + Self { + timestamps: Vec::new(), + window: Duration::from_millis(100), + inputs_per_second: 60, + sustain_until: Instant::now(), + sustain_duration: Duration::from_secs(1), + } + } +} + +impl InputRateTracker { + pub fn record_input(&mut self) { + let now = Instant::now(); + self.timestamps.push(now); + self.prune_old_timestamps(now); + + let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000; + if self.timestamps.len() as u128 >= min_events { + self.sustain_until = now + self.sustain_duration; + } + } + + pub fn is_high_rate(&self) -> bool { + Instant::now() < self.sustain_until + } + + fn prune_old_timestamps(&mut self, now: Instant) { + self.timestamps + .retain(|&t| now.duration_since(t) <= self.window); + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum DrawPhase { + None, + Prepaint, + Paint, + Focus, +} + +pub(crate) const PENDING_INPUT_TIMEOUT: Duration = Duration::from_secs(1); + +/// Pending input for a potential multi-stroke key binding. +pub struct PendingInputStatus<'a> { + keystrokes: &'a [Keystroke], + timeout: Option, +} + +impl<'a> PendingInputStatus<'a> { + /// Returns the keystrokes entered so far. + pub fn keystrokes(&self) -> &'a [Keystroke] { + self.keystrokes + } + + /// Returns the timeout state for flushing this input, if it needs a timeout. + pub fn timeout(&self) -> Option { + self.timeout + } +} + +/// The timeout state for pending input. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PendingInputTimeoutStatus { + duration: Duration, + remaining: Duration, + started_at: Option, + paused: bool, +} + +impl PendingInputTimeoutStatus { + /// Returns the full timeout duration. + pub fn duration(&self) -> Duration { + self.duration + } + + /// Returns the duration remaining before pending input is flushed. + pub fn remaining(&self, cx: &App) -> Duration { + self.started_at + .map(|started_at| { + self.remaining + .saturating_sub(cx.background_executor().now() - started_at) + }) + .unwrap_or(self.remaining) + } + + /// Returns whether the timeout is paused. + pub fn is_paused(&self) -> bool { + self.paused + } +} + +#[derive(Debug)] +struct PendingInputTimeout { + duration: Duration, + remaining: Duration, + state: PendingInputTimeoutState, +} + +#[derive(Debug)] +enum PendingInputTimeoutState { + Running { started_at: Instant, task: Task<()> }, + Paused { pause: PendingInputTimeoutPause }, +} + +#[derive(Debug)] +struct PendingInputTimeoutPause { + owner_id: EntityId, + _release_subscription: Subscription, +} + +impl PendingInputTimeout { + fn is_paused(&self) -> bool { + matches!(&self.state, PendingInputTimeoutState::Paused { .. }) + } + + fn pause(&mut self, pause: PendingInputTimeoutPause, now: Instant) -> bool { + match std::mem::replace(&mut self.state, PendingInputTimeoutState::Paused { pause }) { + PendingInputTimeoutState::Running { started_at, task } => { + self.remaining = self.remaining.saturating_sub(now - started_at); + drop(task); + true + } + previous_state @ PendingInputTimeoutState::Paused { .. } => { + self.state = previous_state; + false + } + } + } + + fn pause_owner_id(&self) -> Option { + match &self.state { + PendingInputTimeoutState::Running { .. } => None, + PendingInputTimeoutState::Paused { pause } => Some(pause.owner_id), + } + } + + fn resume(&mut self, owner_id: EntityId, started_at: Instant, task: Task<()>) -> bool { + match std::mem::replace( + &mut self.state, + PendingInputTimeoutState::Running { started_at, task }, + ) { + PendingInputTimeoutState::Paused { pause } if pause.owner_id == owner_id => true, + previous_state => { + self.state = previous_state; + false + } + } + } + + fn reset_duration(&mut self, duration: Duration) { + self.duration = duration; + self.remaining = duration; + } + + fn status(&self) -> PendingInputTimeoutStatus { + let (started_at, paused) = match &self.state { + PendingInputTimeoutState::Running { started_at, .. } => (Some(*started_at), false), + PendingInputTimeoutState::Paused { .. } => (None, true), + }; + PendingInputTimeoutStatus { + duration: self.duration, + remaining: self.remaining, + started_at, + paused, + } + } +} + +#[derive(Default, Debug)] +struct PendingInput { + keystrokes: SmallVec<[Keystroke; 1]>, + focus: Option, + timeout: Option, +} + +pub(crate) struct ElementStateBox { + pub(crate) inner: Box, + #[cfg(debug_assertions)] + pub(crate) type_name: &'static str, +} + +fn default_bounds(display_id: Option, cx: &mut App) -> WindowBounds { + // TODO, BUG: if you open a window with the currently active window + // on the stack, this will erroneously fallback to `None` + // + // TODO these should be the initial window bounds not considering maximized/fullscreen + let active_window_bounds = cx + .active_window() + .and_then(|w| w.update(cx, |_, window, _| window.window_bounds()).ok()); + + const CASCADE_OFFSET: f32 = 25.0; + + let display = display_id + .map(|id| cx.find_display(id)) + .unwrap_or_else(|| cx.primary_display()); + + let default_placement = || Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE); + + // Use visible_bounds to exclude taskbar/dock areas + let display_bounds = display + .as_ref() + .map(|d| d.visible_bounds()) + .unwrap_or_else(default_placement); + + let ( + Bounds { + origin: base_origin, + size: base_size, + }, + window_bounds_ctor, + ): (_, fn(Bounds) -> WindowBounds) = match active_window_bounds { + Some(bounds) => match bounds { + WindowBounds::Windowed(bounds) => (bounds, WindowBounds::Windowed), + WindowBounds::Maximized(bounds) => (bounds, WindowBounds::Maximized), + WindowBounds::Fullscreen(bounds) => (bounds, WindowBounds::Fullscreen), + }, + None => ( + display + .as_ref() + .map(|d| d.default_bounds()) + .unwrap_or_else(default_placement), + WindowBounds::Windowed, + ), + }; + + let cascade_offset = point(px(CASCADE_OFFSET), px(CASCADE_OFFSET)); + let proposed_origin = base_origin + cascade_offset; + let proposed_bounds = Bounds::new(proposed_origin, base_size); + + let display_right = display_bounds.origin.x + display_bounds.size.width; + let display_bottom = display_bounds.origin.y + display_bounds.size.height; + let window_right = proposed_bounds.origin.x + proposed_bounds.size.width; + let window_bottom = proposed_bounds.origin.y + proposed_bounds.size.height; + + let fits_horizontally = window_right <= display_right; + let fits_vertically = window_bottom <= display_bottom; + + let final_origin = match (fits_horizontally, fits_vertically) { + (true, true) => proposed_origin, + (false, true) => point(display_bounds.origin.x, base_origin.y), + (true, false) => point(base_origin.x, display_bounds.origin.y), + (false, false) => display_bounds.origin, + }; + window_bounds_ctor(Bounds::new(final_origin, base_size)) +} + +impl Window { + pub(crate) fn new( + handle: AnyWindowHandle, + options: WindowOptions, + cx: &mut App, + ) -> Result { + let WindowOptions { + window_bounds, + titlebar, + focus, + show, + kind, + is_movable, + app_owns_titlebar_drag, + inactive_frame_interval, + is_resizable, + is_minimizable, + display_id, + window_background, + app_id, + window_min_size, + window_decorations, + #[cfg_attr( + not(any(target_os = "linux", target_os = "freebsd")), + allow(unused_variables) + )] + icon, + #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] + tabbing_identifier, + } = options; + + let initial_window_title = titlebar + .as_ref() + .and_then(|titlebar| titlebar.title.clone()); + + let window_bounds = window_bounds.unwrap_or_else(|| default_bounds(display_id, cx)); + let mut platform_window = cx.platform.open_window( + handle, + WindowParams { + bounds: window_bounds.get_bounds(), + titlebar, + kind, + is_movable, + app_owns_titlebar_drag, + is_resizable, + is_minimizable, + focus, + show, + display_id, + window_min_size, + app_id: app_id.clone(), + icon, + #[cfg(target_os = "macos")] + tabbing_identifier, + }, + )?; + + let tab_bar_visible = platform_window.tab_bar_visible(); + SystemWindowTabController::init_visible(cx, tab_bar_visible); + if let Some(tabs) = platform_window.tabbed_windows() { + SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); + } + + let display_id = platform_window.display().map(|display| display.id()); + let sprite_atlas = platform_window.sprite_atlas(); + let mouse_position = platform_window.mouse_position(); + let modifiers = platform_window.modifiers(); + let capslock = platform_window.capslock(); + let content_size = platform_window.content_size(); + let scale_factor = platform_window.scale_factor(); + let appearance = platform_window.appearance(); + let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone())); + let invalidator = WindowInvalidator::new(handle.window_id()); + let active = Rc::new(Cell::new(platform_window.is_active())); + let visibility = platform_window.visibility(); + #[cfg(feature = "profiler")] + profiler::journal::record_window_visibility(handle.window_id(), visibility); + let hovered = Rc::new(Cell::new(platform_window.is_hovered())); + let needs_present = Rc::new(Cell::new(false)); + let next_frame_callbacks: Rc>> = Default::default(); + let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default())); + let last_frame_time = Rc::new(Cell::new(None)); + + platform_window + .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); + platform_window.set_background_appearance(window_background); + + match window_bounds { + WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), + WindowBounds::Maximized(_) => platform_window.zoom(), + WindowBounds::Windowed(_) => {} + } + + let accessibility_force_disabled = cx.accessibility_force_disabled; + let a11y_active_flag = Arc::new(AtomicBool::new(false)); + + #[cfg(not(target_family = "wasm"))] + if !accessibility_force_disabled { + let mut initial_root_node = accesskit::Node::new(accesskit::Role::Window); + if let Some(title) = &initial_window_title { + initial_root_node.set_label(title.to_string()); + } + let initial_tree = accesskit::TreeUpdate { + nodes: vec![(ROOT_NODE_ID, initial_root_node)], + tree: Some(accesskit::Tree::new(ROOT_NODE_ID)), + tree_id: accesskit::TreeId::ROOT, + focus: ROOT_NODE_ID, + }; + let (activation_sender, activation_receiver) = async_channel::unbounded::<()>(); + let (deactivation_sender, deactivation_receiver) = async_channel::unbounded::<()>(); + let (action_sender, action_receiver) = + async_channel::unbounded::(); + + platform_window.a11y_init(crate::A11yCallbacks { + activation: { + let active_flag = a11y_active_flag.clone(); + Box::new(move || { + log::info!("Accessibility activated"); + active_flag.store(true, SeqCst); + activation_sender.send_blocking(()).log_err(); + Some(initial_tree.clone()) + }) + }, + action: Box::new(move |request| { + action_sender.send_blocking(request).log_err(); + }), + deactivation: { + let active_flag = a11y_active_flag.clone(); + Box::new(move || { + log::info!("Accessibility deactivated"); + active_flag.store(false, SeqCst); + deactivation_sender.send_blocking(()).log_err(); + }) + }, + }); + + // A11y can be activated at any time, and so we cannot compute a + // correct `TreeUpdate` on-demand. When this happens, we return a + // default empty `TreeUpdate`. + // + // So we force a new frame, which will then send a correct `TreeUpdate`. + let mut async_cx = cx.to_async(); + cx.foreground_executor() + .spawn(async move { + while activation_receiver.recv().await.is_ok() { + handle + .update(&mut async_cx, |_, window, _| window.refresh()) + .log_err(); + } + }) + .detach(); + + let mut async_cx = cx.to_async(); + cx.foreground_executor() + .spawn(async move { + while deactivation_receiver.recv().await.is_ok() { + handle + .update(&mut async_cx, |_, window, _| window.refresh()) + .log_err(); + } + }) + .detach(); + + let mut async_cx = cx.to_async(); + cx.foreground_executor() + .spawn(async move { + while let Ok(request) = action_receiver.recv().await { + handle + .update(&mut async_cx, |_, window, cx| { + window.handle_a11y_action(request, cx); + }) + .log_err(); + } + }) + .detach(); + } + + platform_window.on_close(Box::new({ + let window_id = handle.window_id(); + let mut cx = cx.to_async(); + move || { + let _ = handle.update(&mut cx, |_, window, _| window.remove_window()); + let _ = cx.update(|cx| { + SystemWindowTabController::remove_tab(cx, window_id); + }); + } + })); + platform_window.on_request_frame(Box::new({ + let mut cx = cx.to_async(); + let invalidator = invalidator.clone(); + let active = active.clone(); + let needs_present = needs_present.clone(); + let next_frame_callbacks = next_frame_callbacks.clone(); + let input_rate_tracker = input_rate_tracker.clone(); + let mut deferred_force_render = false; + move |request_frame_options| { + #[cfg(feature = "profiler")] + let (_foreground_turn, window_id) = { + let foreground_turn = profiler::journal::foreground_turn(); + let window_id = handle.window_id(); + profiler::journal::record_platform_signal(profiler::journal::PlatformSignal { + window_id, + signal_at: request_frame_options.signal_at, + handled_at: Instant::now(), + source: request_frame_options.signal_source, + }); + (foreground_turn, window_id) + }; + // This must be checked before accessing App: if this request + // arrived re-entrantly while a draw is on this thread's stack + // (e.g. via a nested message pump in the Windows window + // procedure), drawing would nest draws, and even touching the + // App would panic on its already-mutable borrow. Skip instead; + // the platform leaves the window invalidated (or re-invalidates + // it), so a fresh request arrives once the in-progress draw + // unwinds. Remember force_render so the deferred frame still + // bypasses the view cache. + // + // Returning here skips `complete_frame`, which on Wayland would + // stall the window's frame callbacks (no `surface.commit()`) — + // but calling it would hit the App borrow panic above, and this + // branch is unreachable there in practice: only Windows pumps + // platform events (and thus requests frames) mid-draw. + if draw_in_progress() { + log::debug!("deferring re-entrant window draw request"); + deferred_force_render |= request_frame_options.force_render; + #[cfg(feature = "profiler")] + profiler::journal::record_reentrant_frame_skipped(window_id, Instant::now()); + return; + } + // Take the deferred flag first: `||` short-circuits, and leaving + // the flag set when this request already forces a render would + // force a second, redundant render on the next frame. + let force_render = + mem::take(&mut deferred_force_render) || request_frame_options.force_render; + + let thermal_state = handle + .update(&mut cx, |_, _, cx| cx.thermal_state()) + .log_err(); + + // Throttle frame rate based on conditions: + // - Thermal pressure (Serious/Critical): cap to ~60fps + // - Inactive window (not focused): cap to ~30fps to save energy + let min_frame_interval = if request_frame_options.require_presentation + || (!request_frame_options.force_render + && next_frame_callbacks.borrow().is_empty()) + { + None + } else if !active.get() && !input_rate_tracker.borrow_mut().is_high_rate() { + inactive_frame_interval.map(|interval| (interval, FrameRateLimit::Inactive)) + } else if let Some(ThermalState::Critical | ThermalState::Serious) = thermal_state { + Some((Duration::from_micros(16667), FrameRateLimit::Thermal)) + } else { + None + }; + + let now = Instant::now(); + #[cfg_attr(not(feature = "profiler"), expect(unused_variables))] + if let Some((min_interval, limit)) = min_frame_interval { + if let Some(last_frame) = last_frame_time.get() + && now.duration_since(last_frame) < min_interval + { + // Don't lose a pending forced render to throttling. + deferred_force_render |= force_render; + // Deferred by throttling: ask demand-driven platforms to retry. + handle + .update(&mut cx, |_, window, _| { + window.platform_window.schedule_frame(); + }) + .log_err(); + // The demand that entered this branch (a deferred forced + // render or pending next-frame callbacks) is still + // unserved; platforms that stop requesting frames for + // idle windows need a wakeup to deliver the retry. + invalidator.wake_platform(); + #[cfg(feature = "profiler")] + profiler::journal::record_frame_skipped( + window_id, + Instant::now(), + match limit { + FrameRateLimit::Inactive => { + profiler::journal::FrameSkipReason::InactiveFrameRateLimit + } + FrameRateLimit::Thermal => { + profiler::journal::FrameSkipReason::ThermalFrameRateLimit + } + }, + ); + return; + } + } + last_frame_time.set(Some(now)); + + let pending_next_frame_callbacks = next_frame_callbacks.take(); + if !pending_next_frame_callbacks.is_empty() { + handle + .update(&mut cx, |_, window, cx| { + for callback in pending_next_frame_callbacks { + callback(window, cx); + } + }) + .log_err(); + } + + // Keep presenting if input was recently arriving at a high rate (>= 60fps). + // Once high-rate input is detected, we sustain presentation for 1 second + // to prevent display underclocking during active input. + let needs_present = request_frame_options.require_presentation + || needs_present.get() + || input_rate_tracker.borrow_mut().is_high_rate(); + + let should_draw = invalidator.is_dirty() || force_render; + if should_draw { + measure("frame duration", || { + handle + .update(&mut cx, |_, window, cx| { + if force_render { + // Bypass cached view reuse so we don't replay stale + // atlas tile references after a GPU device recovery. + window.refresh(); + } + let arena_clear_needed = window.draw(cx); + window.present(); + arena_clear_needed.clear(cx); + }) + .log_err(); + }) + } else if needs_present { + handle + .update(&mut cx, |_, window, _| window.present()) + .log_err(); + } + + handle + .update(&mut cx, |_, window, _| { + if window.invalidator.is_dirty() + || !window.next_frame_callbacks.borrow().is_empty() + { + window.platform_window.schedule_frame(); + } + }) + .log_err(); + + // Platforms that stop requesting frames for idle windows only + // deliver another request after a wakeup. If demand remains + // after this frame (the window was re-invalidated mid-draw, or + // animations scheduled next-frame callbacks), re-arm the frame + // source explicitly. + if invalidator.is_dirty() || !next_frame_callbacks.borrow().is_empty() { + invalidator.wake_platform(); + } + #[cfg(feature = "profiler")] + if !should_draw { + profiler::journal::record_frame_skipped( + window_id, + Instant::now(), + profiler::journal::FrameSkipReason::NoRenderNeeded, + ); + } + } + })); + invalidator.set_platform_waker(platform_window.frame_waker()); + platform_window.on_visual_viewport_changed(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, _| window.refresh()) + .log_err(); + } + })); + platform_window.on_insets_changed(Box::new({ + let mut cx = cx.to_async(); + move |_| { + handle + .update(&mut cx, |_, window, _| window.refresh()) + .log_err(); + } + })); + platform_window.on_resize(Box::new({ + let mut cx = cx.to_async(); + move |_, _| { + handle + .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) + .log_err(); + } + })); + platform_window.on_moved(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, cx| window.bounds_changed(cx)) + .log_err(); + } + })); + platform_window.on_appearance_changed(Box::new({ + let cx = cx.to_async(); + let foreground_executor = cx.foreground_executor().clone(); + move || { + let mut cx = cx.clone(); + // Defer the update because changing the AppKit appearance may + // synchronously invoke this callback while App is already borrowed. + foreground_executor + .spawn(async move { + handle + .update(&mut cx, |_, window, cx| window.appearance_changed(cx)) + .log_err(); + }) + .detach(); + } + })); + platform_window.on_button_layout_changed(Box::new({ + let mut cx = cx.to_async(); + move || { + handle + .update(&mut cx, |_, window, cx| window.button_layout_changed(cx)) + .log_err(); + } + })); + platform_window.on_active_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, window, cx| { + window.active.set(active); + window.modifiers = window.platform_window.modifiers(); + window.capslock = window.platform_window.capslock(); + window + .activation_observers + .clone() + .retain(&(), |callback| callback(window, cx)); + + window.bounds_changed(cx); + window.refresh(); + + SystemWindowTabController::update_last_active(cx, window.handle.id); + }) + .log_err(); + } + })); + platform_window.on_visibility_change(Box::new({ + let mut cx = cx.to_async(); + move |_| { + handle + .update(&mut cx, |_, window, cx| { + window.refresh_visibility(cx); + }) + .log_err(); + } + })); + platform_window.on_hover_status_change(Box::new({ + let mut cx = cx.to_async(); + move |active| { + handle + .update(&mut cx, |_, window, _| { + window.hovered.set(active); + window.refresh(); + }) + .log_err(); + } + })); + platform_window.on_input({ + let mut cx = cx.to_async(); + Box::new(move |event| { + handle + .update(&mut cx, |_, window, cx| window.dispatch_event(event, cx)) + .log_err() + .unwrap_or(DispatchEventResult::default()) + }) + }); + platform_window.on_hit_test_window_control({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, _cx| { + for (area, hitbox) in &window.rendered_frame.window_control_hitboxes { + if window.mouse_hit_test.ids.contains(&hitbox.id) { + return Some(*area); + } + } + None + }) + .log_err() + .unwrap_or(None) + }) + }); + platform_window.on_move_tab_to_new_window({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_merge_all_windows({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::merge_all_windows(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_next_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_next_tab(cx, handle.window_id()); + }) + .log_err(); + }) + }); + platform_window.on_select_previous_tab({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, _window, cx| { + SystemWindowTabController::select_previous_tab(cx, handle.window_id()) + }) + .log_err(); + }) + }); + platform_window.on_toggle_tab_bar({ + let mut cx = cx.to_async(); + Box::new(move || { + handle + .update(&mut cx, |_, window, cx| { + let tab_bar_visible = window.platform_window.tab_bar_visible(); + SystemWindowTabController::set_visible(cx, tab_bar_visible); + }) + .log_err(); + }) + }); + + if let Some(app_id) = app_id { + platform_window.set_app_id(&app_id); + } + + platform_window.map_window().unwrap(); + + Ok(Window { + handle, + invalidator, + removed: false, + platform_window, + display_id, + is_resizable, + is_minimizable, + sprite_atlas, + text_system, + text_rendering_mode: cx.text_rendering_mode.clone(), + rem_size: px(16.), + rem_size_override_stack: SmallVec::new(), + viewport_size: content_size, + layout_engine: Some(TaffyLayoutEngine::new()), + root: None, + element_id_stack: SmallVec::default(), + text_style_stack: Vec::new(), + rendered_entity_stack: Vec::new(), + element_offset_stack: Vec::new(), + content_mask_stack: Vec::new(), + element_opacity: 1.0, + requested_autoscroll: None, + last_text_input_configuration: None, + focused_text_input_active: false, + rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + next_frame_callbacks, + next_hitbox_id: HitboxId(0), + next_tooltip_id: TooltipId::default(), + tooltip_bounds: None, + dirty_views: FxHashSet::default(), + focus_listeners: SubscriberSet::new(), + focus_lost_listeners: SubscriberSet::new(), + focus_lost_path: SmallVec::new(), + default_prevented: true, + mouse_position, + mouse_hit_test: HitTest::default(), + modifiers, + capslock, + scale_factor, + bounds_observers: SubscriberSet::new(), + appearance, + appearance_observers: SubscriberSet::new(), + button_layout_observers: SubscriberSet::new(), + active, + visibility, + visibility_observers: SubscriberSet::new(), + hovered, + needs_present, + input_rate_tracker, + #[cfg(feature = "profiler")] + window_profiler: profiler::WindowProfiler::new(handle.window_id())?, + last_input_modality: InputModality::Mouse, + touch_gestures: TouchGestureRecognizer::new( + cx.platform + .gestures() + .map_or_else(GestureTuning::default, |gestures| gestures.tuning()), + ), + touch_prediction_enabled: true, + long_press_timer: None, + long_press_capture: None, + refreshing: false, + activation_observers: SubscriberSet::new(), + focus: None, + focus_enabled: true, + focus_generation: 0, + pending_input: None, + pending_modifier: ModifierState::default(), + pending_input_observers: SubscriberSet::new(), + prompt: None, + client_inset: None, + image_cache_stack: Vec::new(), + captured_hitbox: None, + #[cfg(any(feature = "inspector", debug_assertions))] + inspector: None, + #[cfg(feature = "profiler")] + debug_frame_overlay: crate::debug_overlay::DebugFrameOverlay::new(), + a11y: A11y::new( + a11y_active_flag, + accessibility_force_disabled, + initial_window_title, + ), + }) + } + + pub(crate) fn new_focus_listener( + &self, + value: AnyWindowFocusListener, + ) -> (Subscription, impl FnOnce() + use<>) { + self.focus_listeners.insert((), value) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[expect(missing_docs)] +pub struct DispatchEventResult { + pub propagate: bool, + pub default_prevented: bool, +} + +/// Indicates which region of the window is visible. Content falling outside of this mask will not be +/// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type +/// to leave room to support more complex shapes in the future. +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[repr(C)] +pub struct ContentMask { + /// The bounds + pub bounds: Bounds

, +} + +impl ContentMask { + /// Scale the content mask's pixel units by the given scaling factor. + pub fn scale(&self, factor: f32) -> ContentMask { + ContentMask { + bounds: self.bounds.scale(factor), + } + } + + /// Intersect the content mask with the given content mask. + pub fn intersect(&self, other: &Self) -> Self { + let bounds = self.bounds.intersect(&other.bounds); + ContentMask { bounds } + } +} + +impl Window { + fn mark_view_dirty(&mut self, view_id: EntityId) { + // Mark ancestor views as dirty. If already in the `dirty_views` set, then all its ancestors + // should already be dirty. + for view_id in self + .rendered_frame + .dispatch_tree + .view_path_reversed(view_id) + { + if !self.dirty_views.insert(view_id) { + break; + } + } + } + + pub(crate) fn refresh_visibility(&mut self, cx: &mut App) { + let visibility = self.platform_window.visibility(); + if self.visibility != visibility { + self.visibility = visibility; + #[cfg(feature = "profiler")] + profiler::journal::record_window_visibility(self.handle.window_id(), visibility); + self.visibility_observers + .clone() + .retain(&(), |callback| callback(visibility, self, cx)); + } + #[cfg(feature = "profiler")] + if self.invalidator.is_dirty() || self.needs_present.get() { + profiler::journal::record_frame_pending(self.handle.window_id(), Instant::now()); + } + } + + /// Whether the platform is presenting this window's frames (see + /// [`WindowVisibility`]). + pub fn visibility(&self) -> WindowVisibility { + self.visibility + } + + /// Whether frames drawn for this window will be shown. + /// + /// This is not the window's shown/hidden state: a shown window that is + /// fully behind another window, minimized, or on a sleeping display is not + /// visible here. + pub fn is_visible(&self) -> bool { + self.visibility.is_visible() + } + + /// Registers a callback to be invoked when the window's visibility changes. + pub fn observe_window_visibility( + &self, + mut callback: impl FnMut(WindowVisibility, &mut Window, &mut App) + 'static, + ) -> Subscription { + let (subscription, activate) = self.visibility_observers.insert( + (), + Box::new(move |visibility, window, cx| { + callback(visibility, window, cx); + true + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window appearance changes. + pub fn observe_window_appearance( + &self, + mut callback: impl FnMut(&mut Window, &mut App) + 'static, + ) -> Subscription { + let (subscription, activate) = self.appearance_observers.insert( + (), + Box::new(move |window, cx| { + callback(window, cx); + true + }), + ); + activate(); + subscription + } + + /// Registers a callback to be invoked when the window button layout changes. + pub fn observe_button_layout_changed( + &self, + mut callback: impl FnMut(&mut Window, &mut App) + 'static, + ) -> Subscription { + let (subscription, activate) = self.button_layout_observers.insert( + (), + Box::new(move |window, cx| { + callback(window, cx); + true + }), + ); + activate(); + subscription + } + + /// Replaces the root entity of the window with a new one. + pub fn replace_root( + &mut self, + cx: &mut App, + build_view: impl FnOnce(&mut Window, &mut Context) -> E, + ) -> Entity + where + E: 'static + Render, + { + let view = cx.new(|cx| build_view(self, cx)); + self.root = Some(view.clone().into()); + self.refresh(); + view + } + + /// Returns the root entity of the window, if it has one. + pub fn root(&self) -> Option>> + where + E: 'static + Render, + { + self.root + .as_ref() + .map(|view| view.clone().downcast::().ok()) + } + + /// Obtain a handle to the window that belongs to this context. + pub fn window_handle(&self) -> AnyWindowHandle { + self.handle + } + + /// Mark the window as dirty, scheduling it to be redrawn on the next frame. + pub fn refresh(&mut self) { + if self.invalidator.not_drawing() { + self.refreshing = true; + self.invalidator.set_dirty(true); + } + } + + /// Close this window. + pub fn remove_window(&mut self) { + self.removed = true; + } + + /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. + pub fn focused(&self, cx: &App) -> Option { + self.focus + .and_then(|id| FocusHandle::for_id(id, &cx.focus_handles)) + } + + /// While focus-lost listeners are being dispatched, returns the closest ancestor of the + /// previously focused element that can still receive focus, making it a suitable target + /// for focus restoration. Returns `None` at all other times, or when no such ancestor exists. + pub fn focus_lost_restore_target(&self, cx: &App) -> Option { + let (_leaf, ancestors) = self.focus_lost_path.split_last()?; + ancestors.iter().rev().find_map(|id| { + self.rendered_frame.dispatch_tree.focusable_node_id(*id)?; + FocusHandle::for_id(*id, &cx.focus_handles) + }) + } + + /// Move focus to the element associated with the given [`FocusHandle`]. + pub fn focus(&mut self, handle: &FocusHandle, cx: &mut App) { + if !self.focus_enabled || self.focus == Some(handle.id) { + return; + } + + self.focus = Some(handle.id); + self.focus_generation = self.focus_generation.wrapping_add(1); + self.clear_pending_keystrokes(cx); + + self.refresh(); + } + + /// Remove focus from all elements within this context's window. + pub fn blur(&mut self, cx: &mut App) { + self.clear_pending_keystrokes(cx); + + if !self.focus_enabled { + return; + } + + if self.focus.is_some() { + self.focus_generation = self.focus_generation.wrapping_add(1); + } + self.focus = None; + self.refresh(); + } + + /// Blur the window and don't allow anything in it to be focused again. + pub fn disable_focus(&mut self, cx: &mut App) { + self.blur(cx); + self.focus_enabled = false; + } + + /// Move focus to next tab stop. + pub fn focus_next(&mut self, cx: &mut App) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) { + self.focus(&handle, cx) + } + } + + /// Move focus to previous tab stop. + pub fn focus_prev(&mut self, cx: &mut App) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) { + self.focus(&handle, cx) + } + } + + /// Accessor for the text system. + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + /// The current text style. Which is composed of all the style refinements provided to `with_text_style`. + pub fn text_style(&self) -> TextStyle { + let mut style = TextStyle::default(); + for refinement in &self.text_style_stack { + style.refine(refinement); + } + style + } + + /// Check if the platform window is maximized. + /// + /// On some platforms (namely Windows) this is different than the bounds being the size of the display + pub fn is_maximized(&self) -> bool { + self.platform_window.is_maximized() + } + + /// request a certain window decoration (Wayland) + pub fn request_decorations(&self, decorations: WindowDecorations) { + self.platform_window.request_decorations(decorations); + } + + /// Set the exclusive zone for a layer-shell surface: how much screen space it + /// reserves so other surfaces avoid occluding it (e.g. a panel reserving space). + /// Positive values reserve that distance from the anchored edge, 0 lets the + /// surface be moved out of others' exclusive zones, and -1 ignores reserved + /// space and may extend under other surfaces. (Wayland layer-shell windows only) + pub fn set_exclusive_zone(&self, zone: Pixels) { + self.platform_window.set_exclusive_zone(zone); + } + + /// Set which anchored edge a layer-shell surface's exclusive zone applies to. + /// This is only needed to disambiguate a corner-anchored surface; otherwise the + /// edge is deduced from the anchor. The edge must be a single edge the surface + /// is anchored to, or it is ignored. (Wayland layer-shell windows only) + #[cfg(all(target_os = "linux", feature = "wayland"))] + pub fn set_exclusive_edge(&self, edge: crate::layer_shell::Anchor) { + self.platform_window.set_exclusive_edge(edge); + } + + /// Start an interactive window resize operation if this window is resizable. + pub fn start_window_resize(&self, edge: ResizeEdge) { + if self.is_resizable { + self.platform_window.start_window_resize(edge); + } + } + + /// Linux (wayland) only: Set the window's input region, the area that receives pointer + /// and touch input. Events outside it pass through to whatever is below the window. + /// + /// - `Some(rects)` restricts input to the union of `rects`, in window coordinates. + /// - `Some(&[])` is an empty region, so the window receives no pointer or touch input. + /// - `None` resets the region to the default, so the whole window receives input again. + pub fn set_input_region(&self, region: Option<&[Bounds]>) { + self.platform_window.set_input_region(region); + } + + /// Return the `WindowBounds` to indicate that how a window should be opened + /// after it has been closed + pub fn window_bounds(&self) -> WindowBounds { + self.platform_window.window_bounds() + } + + /// Return the `WindowBounds` excluding insets (Wayland and X11) + pub fn inner_window_bounds(&self) -> WindowBounds { + self.platform_window.inner_window_bounds() + } + + /// Encode the window's native restorable state into an opaque blob. + /// Returns `None` on platforms without native state restoration or if encoding fails. + pub fn native_window_state(&self) -> Option> { + self.platform_window.native_window_state() + } + + /// Restore the window's native state from a blob previously produced + /// by [`Window::native_window_state`]. A no-op on platforms without native state restoration. + pub fn restore_native_window_state(&self, state: &[u8]) { + self.platform_window.restore_native_window_state(state); + } + + /// Dispatch the given action on the currently focused element. + pub fn dispatch_action(&mut self, action: Box, cx: &mut App) { + let focus_id = self.focused(cx).map(|handle| handle.id); + + let window = self.handle; + cx.defer(move |cx| { + window + .update(cx, |_, window, cx| { + let node_id = window.focus_node_id_in_rendered_frame(focus_id); + window.dispatch_action_on_node(node_id, action.as_ref(), cx); + }) + .log_err(); + }) + } + + pub(crate) fn dispatch_keystroke_observers( + &mut self, + keystroke: &Keystroke, + input_preference: InputPreference, + action: Option<&dyn Action>, + context_stack: Vec, + cx: &mut App, + ) { + cx.keystroke_observers.clone().retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: keystroke.clone(), + input_preference, + action: action.map(|action| action.boxed_clone()), + context_stack: context_stack.clone(), + }, + self, + cx, + ) + }); + } + + pub(crate) fn dispatch_keystroke_interceptors( + &mut self, + keystroke: &Keystroke, + input_preference: InputPreference, + context_stack: Vec, + cx: &mut App, + ) { + cx.keystroke_interceptors + .clone() + .retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: keystroke.clone(), + input_preference, + action: None, + context_stack: context_stack.clone(), + }, + self, + cx, + ) + }); + } + + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities + /// that are currently on the stack to be returned to the app. + pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) { + let handle = self.handle; + cx.defer(move |cx| { + handle.update(cx, |_, window, cx| f(window, cx)).ok(); + }); + } + + /// Subscribe to events emitted by a entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. + pub fn observe( + &mut self, + observed: &Entity, + cx: &mut App, + mut on_notify: impl FnMut(Entity, &mut Window, &mut App) + 'static, + ) -> Subscription { + let entity_id = observed.entity_id(); + let observed = observed.downgrade(); + let window_handle = self.handle; + cx.new_observer( + entity_id, + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some(handle) = observed.upgrade() { + on_notify(handle, window, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ) + } + + /// Subscribe to events emitted by a entity. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity, the event, and a window context for the current window. + pub fn subscribe( + &mut self, + entity: &Entity, + cx: &mut App, + mut on_event: impl FnMut(Entity, &Evt, &mut Window, &mut App) + 'static, + ) -> Subscription + where + Emitter: EventEmitter, + Evt: 'static, + { + let entity_id = entity.entity_id(); + let handle = entity.downgrade(); + let window_handle = self.handle; + cx.new_subscription( + entity_id, + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, window, cx| { + if let Some(entity) = handle.upgrade() { + let event = event.downcast_ref().expect("invalid event type"); + on_event(entity, event, window, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ), + ) + } + + /// Register a callback to be invoked when the given `Entity` is released. + pub fn observe_release( + &self, + entity: &Entity, + cx: &mut App, + mut on_release: impl FnOnce(&mut T, &mut Window, &mut App) + 'static, + ) -> Subscription + where + T: 'static, + { + let entity_id = entity.entity_id(); + let window_handle = self.handle; + let (subscription, activate) = cx.release_listeners.insert( + entity_id, + Box::new(move |entity, cx| { + let entity = entity.downcast_mut().expect("invalid entity type"); + let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx)); + }), + ); + activate(); + subscription + } + + /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across + /// await points in async code. + pub fn to_async(&self, cx: &App) -> AsyncWindowContext { + AsyncWindowContext::new_context(cx.to_async(), self.handle) + } + + /// Schedule the given closure to be run directly after the current frame is rendered. + pub fn on_next_frame(&self, callback: impl FnOnce(&mut Window, &mut App) + 'static) { + RefCell::borrow_mut(&self.next_frame_callbacks).push(Box::new(callback)); + self.platform_window.schedule_frame(); + // Next-frame callbacks create frame demand without dirtying the + // window, so the platform's frame source must be woken explicitly. + self.invalidator.wake_platform(); + } + + /// Schedule a frame to be drawn on the next animation frame. + /// + /// This is useful for elements that need to animate continuously, such as a video player or an animated GIF. + /// It will cause the window to redraw on the next frame, even if no other changes have occurred. + /// + /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. + /// + /// Callers driving purely decorative animations (spinners, pulses, and the + /// like) should prefer [`AnimationExt::with_animation`](crate::AnimationExt::with_animation), + /// which automatically respects [`App::reduce_motion`]. When using this + /// method directly for decorative motion, check [`App::reduce_motion`] + /// and skip the frame request when it is set. + pub fn request_animation_frame(&self) { + let entity = self.current_view(); + self.on_next_frame(move |_, cx| cx.notify(entity)); + } + + /// Runs all callbacks scheduled via [`Self::on_next_frame`], returning how many ran. + /// + /// Tests have no platform frame loop, so this simulates the delivery of the + /// next frame. + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_next_frame(&mut self, cx: &mut App) -> usize { + let callbacks = self.next_frame_callbacks.take(); + let count = callbacks.len(); + for callback in callbacks { + callback(self, cx); + } + count + } + + /// Spawn the future returned by the given closure on the application thread pool. + /// The closure is provided a handle to the current window and an `AsyncWindowContext` for + /// use within your future. + #[track_caller] + pub fn spawn(&self, cx: &App, f: AsyncFn) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, + { + let handle = self.handle; + cx.spawn(async move |app| { + let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); + f(&mut async_window_cx).await + }) + } + + /// Spawn the future returned by the given closure on the application thread + /// pool, with the given priority. The closure is provided a handle to the + /// current window and an `AsyncWindowContext` for use within your future. + #[track_caller] + pub fn spawn_with_priority( + &self, + priority: Priority, + cx: &App, + f: AsyncFn, + ) -> Task + where + R: 'static, + AsyncFn: AsyncFnOnce(&mut AsyncWindowContext) -> R + 'static, + { + let handle = self.handle; + cx.spawn_with_priority(priority, async move |app| { + let mut async_window_cx = AsyncWindowContext::new_context(app.clone(), handle); + f(&mut async_window_cx).await + }) + } + + /// Notify the window that its bounds have changed. + /// + /// This updates internal state like `viewport_size` and `scale_factor` from + /// the platform window, then notifies observers. Normally called automatically + /// by the platform's resize callback, but exposed publicly for test infrastructure. + pub fn bounds_changed(&mut self, cx: &mut App) { + self.scale_factor = self.platform_window.scale_factor(); + self.viewport_size = self.platform_window.content_size(); + self.display_id = self.platform_window.display().map(|display| display.id()); + self.mouse_position = self.platform_window.mouse_position(); + + self.refresh(); + + self.bounds_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. + pub fn bounds(&self) -> Bounds { + self.platform_window.bounds() + } + + /// Renders the current frame's scene to a texture and returns the pixel data as an RGBA image. + /// This does not present the frame to screen - useful for visual testing where we want + /// to capture what would be rendered without displaying it or requiring the window to be visible. + #[cfg(any(test, feature = "test-support"))] + pub fn render_to_image(&self) -> anyhow::Result { + self.platform_window + .render_to_image(&self.rendered_frame.scene) + } + + /// Returns the quads in the most recently rendered frame's scene, so tests can assert on + /// painted output without rasterizing the frame. Quad bounds are in scaled pixels and are + /// not clipped; each quad carries the content mask it will be clipped to when drawn. Quads + /// whose bounds don't intersect their content mask are culled at paint time and won't appear. + #[cfg(any(test, feature = "test-support"))] + pub fn painted_quads(&self) -> Vec { + self.rendered_frame.scene.quads.clone() + } + + /// Returns the underlines in the most recently rendered frame's scene. + #[cfg(any(test, feature = "test-support"))] + pub fn painted_underlines(&self) -> Vec { + self.rendered_frame.scene.underlines.clone() + } + + /// Set the content size of the window. + pub fn resize(&mut self, size: Size) { + self.platform_window.resize(size); + } + + /// Returns whether or not the window is currently fullscreen + pub fn is_fullscreen(&self) -> bool { + self.platform_window.is_fullscreen() + } + + /// Returns whether the window is currently in simple (borderless) fullscreen, + /// where it covers the entire screen including the menu bar and notch area. + /// Always `false` on platforms other than macOS. + pub fn is_simple_fullscreen(&self) -> bool { + self.platform_window.is_simple_fullscreen() + } + + pub(crate) fn appearance_changed(&mut self, cx: &mut App) { + self.appearance = self.platform_window.appearance(); + + self.appearance_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + pub(crate) fn button_layout_changed(&mut self, cx: &mut App) { + self.button_layout_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + /// Returns the appearance of the current window. + pub fn appearance(&self) -> WindowAppearance { + self.appearance + } + + /// Returns the size of the drawable area within the window. + pub fn viewport_size(&self) -> Size { + self.viewport_size + } + + /// Returns the platform's visible viewport in window-local logical pixels. + /// + /// Unlike `viewport_size`, this can shrink or move when the keyboard opens. + /// During drawing this is a consistent frame snapshot. Outside drawing it + /// reflects the latest platform sample, not a synchronous geometry query. + pub fn visual_viewport_bounds(&self) -> Bounds { + self.platform_window.visual_viewport_bounds() + } + + /// Returns a conservative rectangle avoiding platform-known obscured content. + /// + /// Intersects the visual viewport with the full layout area inset by system + /// safe areas and keyboard occlusion. Unknown overlays cannot be excluded. + pub fn fully_visible_bounds(&self) -> Bounds { + let insets = self.platform_window.insets().effective(); + let viewport = self.viewport_size(); + let left = insets.left.max(Pixels::ZERO).min(viewport.width); + let top = insets.top.max(Pixels::ZERO).min(viewport.height); + let right = (viewport.width - insets.right.max(Pixels::ZERO)).max(left); + let bottom = (viewport.height - insets.bottom.max(Pixels::ZERO)).max(top); + let safe_bounds = Bounds::from_corners(point(left, top), point(right, bottom)); + let mut visible = safe_bounds.intersect(&self.visual_viewport_bounds()); + visible.size.width = visible.size.width.max(Pixels::ZERO); + visible.size.height = visible.size.height.max(Pixels::ZERO); + visible + } + + /// Requests the virtual keyboard for the currently focused text input. + /// + /// Call from a user gesture on platforms that require one. The platform may + /// decline the request; this does not change focus or the layout viewport. + pub fn request_virtual_keyboard(&self) { + self.platform_window.show_soft_keyboard(); + } + + /// Requests dismissal of the virtual keyboard without changing GPUI focus. + pub fn dismiss_virtual_keyboard(&self) { + self.platform_window.hide_soft_keyboard(); + } + + /// Returns whether this window is focused by the operating system (receiving key events). + pub fn is_window_active(&self) -> bool { + self.active.get() + } + + /// Returns whether this window is considered to be the window + /// that currently owns the mouse cursor. + /// On mac, this is equivalent to `is_window_active`. + pub fn is_window_hovered(&self) -> bool { + if cfg!(any( + target_os = "windows", + target_os = "linux", + target_os = "freebsd" + )) { + self.hovered.get() + } else { + self.is_window_active() + } + } + + /// Toggle zoom on the window. + pub fn zoom_window(&self) { + self.platform_window.zoom(); + } + + /// Opens the native title bar context menu, useful when implementing client side decorations (Wayland and X11) + pub fn show_window_menu(&self, position: Point) { + self.platform_window.show_window_menu(position) + } + + /// Handle window movement for Linux and macOS. + /// Tells the compositor to take control of window movement (Wayland and X11) + /// + /// Events may not be received during a move operation. + pub fn start_window_move(&self) { + self.platform_window.start_window_move() + } + + /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11) + pub fn set_client_inset(&mut self, inset: Pixels) { + self.client_inset = Some(inset); + self.platform_window.set_client_inset(inset); + } + + /// Returns the client_inset value by [`Self::set_client_inset`]. + pub fn client_inset(&self) -> Option { + self.client_inset + } + + /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) + pub fn window_decorations(&self) -> Decorations { + self.platform_window.window_decorations() + } + + /// Returns whether this window is resizable. + pub fn is_resizable(&self) -> bool { + self.is_resizable + } + + /// Returns whether this window is minimizable. + pub fn is_minimizable(&self) -> bool { + self.is_minimizable + } + + /// Returns the controls supported by the platform. + pub fn window_controls(&self) -> WindowControls { + self.platform_window.window_controls() + } + + /// Updates the window's title at the platform level. + pub fn set_window_title(&mut self, title: &str) { + self.platform_window.set_title(title); + self.a11y.set_window_title(title.to_string()); + } + + /// Sets the position of the macOS traffic light buttons. + #[cfg(target_os = "macos")] + pub fn set_traffic_light_position(&self, position: Point) { + self.platform_window.set_traffic_light_position(position); + } + + /// Sets the application identifier. + pub fn set_app_id(&mut self, app_id: &str) { + self.platform_window.set_app_id(app_id); + } + + /// Sets the window background appearance. + pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + self.platform_window + .set_background_appearance(background_appearance); + } + + /// Mark the window as dirty at the platform level. + pub fn set_window_edited(&mut self, edited: bool) { + self.platform_window.set_edited(edited); + } + + /// Set the path of the file this window represents. + /// On macOS, this sets the window's accessibility document property (AXDocument). + pub fn set_document_path(&self, path: Option<&std::path::Path>) { + self.platform_window.set_document_path(path); + } + + /// Determine the display on which the window is visible. + pub fn display(&self, cx: &App) -> Option> { + cx.platform + .displays() + .into_iter() + .find(|display| Some(display.id()) == self.display_id) + } + + /// Show the platform character palette. + pub fn show_character_palette(&self) { + self.platform_window.show_character_palette(); + } + + /// The scale factor of the display associated with the window. For example, it could + /// return 2.0 for a "retina" display, indicating that each logical pixel should actually + /// be rendered as two pixels on screen. + pub fn scale_factor(&self) -> f32 { + self.scale_factor + } + + /// Overrides the display scale factor for tests. + #[cfg(any(test, feature = "test-support"))] + pub fn set_scale_factor(&mut self, scale_factor: f32) { + self.scale_factor = scale_factor; + self.refresh(); + } + + /// The size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn rem_size(&self) -> Pixels { + self.rem_size_override_stack + .last() + .copied() + .unwrap_or(self.rem_size) + } + + /// Sets the size of an em for the base font of the application. Adjusting this value allows the + /// UI to scale, just like zooming a web page. + pub fn set_rem_size(&mut self, rem_size: impl Into) { + self.rem_size = rem_size.into(); + } + + /// Acquire a globally unique identifier for the given ElementId. + /// Only valid for the duration of the provided closure. + pub fn with_global_id( + &mut self, + element_id: ElementId, + f: impl FnOnce(&GlobalElementId, &mut Self) -> R, + ) -> R { + self.with_id(element_id, |this| { + let global_id = GlobalElementId(Arc::from(&*this.element_id_stack)); + + f(&global_id, this) + }) + } + + /// Calls the provided closure with the element ID pushed on the stack. + #[inline] + pub fn with_id( + &mut self, + element_id: impl Into, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.element_id_stack.push(element_id.into()); + let result = f(self); + self.element_id_stack.pop(); + result + } + + /// Executes the provided function with the specified rem size. + /// + /// This method must only be called as part of element drawing. + // This function is called in a highly recursive manner in editor + // prepainting, make sure its inlined to reduce the stack burden + #[inline] + pub fn with_rem_size(&mut self, rem_size: Option>, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + if let Some(rem_size) = rem_size { + self.rem_size_override_stack.push(rem_size.into()); + let result = f(self); + self.rem_size_override_stack.pop(); + result + } else { + f(self) + } + } + + /// The line height associated with the current text style. + pub fn line_height(&self) -> Pixels { + self.text_style().line_height_in_pixels(self.rem_size()) + } + + /// Rounds a logical value to the nearest device pixel. + #[inline] + pub fn pixel_snap(&self, value: Pixels) -> Pixels { + px(round_to_device_pixel(value.0, self.scale_factor()) / self.scale_factor()) + } + + /// f64 variant of [`Self::pixel_snap`]. + #[inline] + pub fn pixel_snap_f64(&self, value: f64) -> f64 { + let scale_factor = f64::from(self.scale_factor()); + round_half_toward_zero_f64(value * scale_factor) / scale_factor + } + + /// Snaps a bounds' origin and size to the nearest device pixel. + #[inline] + pub fn pixel_snap_bounds(&self, bounds: Bounds) -> Bounds { + bounds.map(|c| self.pixel_snap(c)) + } + + /// Snaps a point's coordinates to the nearest device pixel. + #[inline] + pub fn pixel_snap_point(&self, position: Point) -> Point { + position.map(|c| self.pixel_snap(c)) + } + + /// Returns the snapped device-space bounds used to paint an underline. + pub fn underline_bounds( + &self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) -> Bounds { + let scale_factor = self.scale_factor(); + let thickness = self.snap_stroke(style.thickness); + let height = if style.wavy { + ScaledPixels(thickness.0 * 3.) + } else { + thickness + }; + Bounds { + origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), + size: size(self.snap_stroke(width), height), + } + } + + /// Paints an underline excluding absolute device-space horizontal spans. + pub fn paint_underline_with_exclusions( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + exclusions: &[Range], + ) { + self.invalidator.debug_assert_paint(); + let underline = self.underline(origin, width, style); + if exclusions.is_empty() { + self.next_frame.scene.insert_primitive(underline); + return; + } + + let bounds = underline.bounds.intersect(&underline.content_mask.bounds); + if bounds.is_empty() { + return; + } + + let mut exclusions = exclusions + .iter() + .filter_map(|span| { + let start = span.start.max(bounds.left()); + let end = span.end.min(bounds.right()); + (start < end).then_some(start..end) + }) + .collect::>(); + if exclusions.is_empty() { + self.next_frame.scene.insert_primitive(underline); + return; + } + exclusions.sort_unstable_by_key(|span| span.start); + + let mut paint_span = |start, end| { + self.next_frame.scene.insert_primitive(Underline { + content_mask: ContentMask { + bounds: Bounds::from_corners( + point(start, bounds.top()), + point(end, bounds.bottom()), + ), + }, + ..underline + }); + }; + let mut start = bounds.left(); + for exclusion in exclusions { + if start < exclusion.start { + paint_span(start, exclusion.start); + } + start = start.max(exclusion.end); + } + if start < bounds.right() { + paint_span(start, bounds.right()); + } + } + + fn underline(&self, origin: Point, width: Pixels, style: &UnderlineStyle) -> Underline { + Underline { + order: 0, + pad: 0, + bounds: self.underline_bounds(origin, width, style), + content_mask: self.snapped_content_mask(), + color: style + .color + .unwrap_or_default() + .opacity(self.element_opacity()), + thickness: self.snap_stroke(style.thickness), + wavy: style.wavy.into(), + } + } + + #[inline] + fn snap_bounds(&self, bounds: Bounds) -> Bounds { + let scale_factor = self.scale_factor(); + let left = round_to_device_pixel(bounds.left().0, scale_factor); + let top = round_to_device_pixel(bounds.top().0, scale_factor); + let right = round_to_device_pixel(bounds.right().0, scale_factor).max(left); + let bottom = round_to_device_pixel(bounds.bottom().0, scale_factor).max(top); + Bounds::from_corners( + point(ScaledPixels(left), ScaledPixels(top)), + point(ScaledPixels(right), ScaledPixels(bottom)), + ) + } + + /// Rounds half-to-zero but clamps any non-zero input up to 1 dp so thin strokes do not disappear. + #[inline] + fn snap_stroke(&self, value: Pixels) -> ScaledPixels { + ScaledPixels(round_stroke_to_device_pixel(value.0, self.scale_factor())) + } + + #[inline] + fn snap_border_widths(&self, edges: Edges) -> Edges { + edges.map(|e| self.snap_stroke(*e)) + } + + /// Floors the near edge and ceils the far edge, producing a strict superset of the raw region. + #[inline] + fn cover_bounds(&self, bounds: Bounds) -> Bounds { + let scale_factor = self.scale_factor(); + let left = floor_to_device_pixel(bounds.left().0, scale_factor); + let top = floor_to_device_pixel(bounds.top().0, scale_factor); + let right = ceil_to_device_pixel(bounds.right().0, scale_factor).max(left); + let bottom = ceil_to_device_pixel(bounds.bottom().0, scale_factor).max(top); + Bounds::from_corners( + point(ScaledPixels(left), ScaledPixels(top)), + point(ScaledPixels(right), ScaledPixels(bottom)), + ) + } + + #[inline] + fn snapped_content_mask(&self) -> ContentMask { + ContentMask { + bounds: self.cover_bounds(self.content_mask().bounds), + } + } + + /// Call to prevent the default action of an event. Currently only used to prevent + /// parent elements from becoming focused on mouse down. + pub fn prevent_default(&mut self) { + self.default_prevented = true; + } + + /// Obtain whether default has been prevented for the event currently being dispatched. + pub fn default_prevented(&self) -> bool { + self.default_prevented + } + + /// Determine whether the given action is available along the dispatch path to the currently focused element. + pub fn is_action_available(&self, action: &dyn Action, cx: &App) -> bool { + let node_id = + self.focus_node_id_in_rendered_frame(self.focused(cx).map(|handle| handle.id)); + self.rendered_frame + .dispatch_tree + .is_action_available(action, node_id) + } + + /// Determine whether the given action is available along the dispatch path to the given focus_handle. + pub fn is_action_available_in(&self, action: &dyn Action, focus_handle: &FocusHandle) -> bool { + let node_id = self.focus_node_id_in_rendered_frame(Some(focus_handle.id)); + self.rendered_frame + .dispatch_tree + .is_action_available(action, node_id) + } + + /// The position of the mouse relative to the window. + pub fn mouse_position(&self) -> Point { + self.mouse_position + } + + /// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up + /// events will be routed to listeners that check this hitbox's `is_hovered` status, + /// regardless of actual hit testing. This enables drag operations that continue + /// even when the pointer moves outside the element's bounds. + /// + /// The capture is automatically released on mouse up. + pub fn capture_pointer(&mut self, hitbox_id: HitboxId) { + self.captured_hitbox = Some(hitbox_id); + } + + /// Releases any active pointer capture. + pub fn release_pointer(&mut self) { + self.captured_hitbox = None; + } + + /// Returns the hitbox that has captured the pointer, if any. + pub fn captured_hitbox(&self) -> Option { + self.captured_hitbox + } + + /// Captures the current long press for the given entity. + /// + /// The capture is released when the gesture ends or is cancelled, or when + /// a replacement touch begins. A listener must also call + /// [`Self::prevent_default`] on the started event to claim the gesture. + pub fn capture_long_press(&mut self, entity: &Entity) { + self.long_press_capture = Some(entity.entity_id()); + } + + /// Returns whether the given entity has captured the current long press. + pub fn has_long_press_capture(&self, entity: &Entity) -> bool { + self.long_press_capture == Some(entity.entity_id()) + } + + /// The current state of the keyboard's modifiers + pub fn modifiers(&self) -> Modifiers { + self.modifiers + } + + /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.) + /// This is used for focus-visible styling to show focus indicators only for keyboard navigation. + pub fn last_input_was_keyboard(&self) -> bool { + self.last_input_modality == InputModality::Keyboard + } + + pub(crate) fn last_input_was_touch(&self) -> bool { + self.last_input_modality == InputModality::Touch + } + + /// The current state of the keyboard's capslock + pub fn capslock(&self) -> Capslock { + self.capslock + } + + /// Produces a new frame and assigns it to `rendered_frame`. To actually show + /// the contents of the new [`Scene`], use [`Self::present`]. + #[profiling::function] + pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded { + // Drain every draw in profiler builds so a previous frame's + // first-invalidation timestamp can't be attributed to this one. + #[cfg(feature = "profiler")] + let frame_dirty = self.invalidator.take_frame_dirty(); + #[cfg(feature = "profiler")] + self.window_profiler.begin_draw(); + + // Set up the per-App arena for element allocation during this draw. + // This ensures that multiple test Apps have isolated arenas. + let arena_scope = ElementArenaScope::enter(&cx.element_arena); + + if self.platform_window.prepare_frame() { + self.refresh(); + } + self.invalidate_entities(); + cx.entities.clear_accessed(); + debug_assert!(self.rendered_entity_stack.is_empty()); + self.invalidator.set_dirty(false); + self.requested_autoscroll = None; + + // Restore the previously-used input handler. + // Place it back into a None slot (left by a previous .take()) so that + // cached paint_range indices in reuse_paint find the handler at the + // expected position. + if let Some(input_handler) = self.platform_window.take_input_handler() { + if let Some(slot) = self + .rendered_frame + .input_handlers + .iter_mut() + .rev() + .find(|h| h.is_none()) + { + *slot = Some(input_handler); + } else { + self.rendered_frame.input_handlers.push(Some(input_handler)); + } + } + if !cx.mode.skip_drawing() { + self.draw_roots(cx); + #[cfg(feature = "profiler")] + { + let viewport_size = self.viewport_size; + let scale_factor = self.scale_factor(); + self.debug_frame_overlay.paint( + &mut self.next_frame.scene, + viewport_size, + scale_factor, + ); + } + } + self.dirty_views.clear(); + self.next_frame.window_active = self.active.get(); + + // Register requested input handler with the platform window. + // Use .take() instead of .pop() to preserve Vec length, so that cached + // paint_range indices remain valid for reuse_paint on the next frame. + // Search backwards to find the last Some entry, since reuse_paint may + // have copied None slots from the previous frame. (Fixes #50456) + let focused_text_input_active = if let Some(mut input_handler) = self + .next_frame + .input_handlers + .iter_mut() + .rev() + .find_map(|h| h.take()) + { + let accepts_text_input = input_handler.accepts_text_input(self, cx); + self.platform_window.set_input_handler(input_handler); + accepts_text_input + } else { + false + }; + self.apply_text_input_configuration(cx); + if focused_text_input_active != self.focused_text_input_active { + self.focused_text_input_active = focused_text_input_active; + self.platform_window + .text_input_state_changed(if focused_text_input_active { + TextInputStateChange::FocusGained + } else { + TextInputStateChange::FocusLost + }); + } + + self.layout_engine.as_mut().unwrap().clear(); + self.text_system().finish_frame(); + self.next_frame.finish(&mut self.rendered_frame); + + self.invalidator.set_phase(DrawPhase::Focus); + let previous_focus_path = self.rendered_frame.focus_path(); + let previous_window_active = self.rendered_frame.window_active; + mem::swap(&mut self.rendered_frame, &mut self.next_frame); + self.next_frame.clear(); + let current_focus_path = self.rendered_frame.focus_path(); + let current_window_active = self.rendered_frame.window_active; + let mut focus_before_listeners = self.focus; + + if previous_focus_path != current_focus_path + || previous_window_active != current_window_active + { + if !previous_focus_path.is_empty() && current_focus_path.is_empty() { + self.focus_lost_path = previous_focus_path.clone(); + self.focus_lost_listeners + .clone() + .retain(&(), |listener| listener(self, cx)); + self.focus_lost_path = SmallVec::new(); + // The focus-lost fallback (e.g. a workspace refocusing itself) may target + // an element that isn't part of the element tree, in which case scheduling + // a redraw below would dispatch focus-lost again, looping forever. Only + // track focus movement caused by the focus listeners. + focus_before_listeners = self.focus; + } + + let event = WindowFocusEvent { + previous_focus_path: if previous_window_active { + previous_focus_path + } else { + Default::default() + }, + current_focus_path: if current_window_active { + current_focus_path + } else { + Default::default() + }, + }; + self.focus_listeners + .clone() + .retain(&(), |listener| listener(&event, self, cx)); + } + + debug_assert!(self.rendered_entity_stack.is_empty()); + self.record_entities_accessed(cx); + self.reset_cursor_style(cx); + self.refreshing = false; + self.invalidator.set_phase(DrawPhase::None); + // Focus listeners may move focus (e.g. a dock forwarding focus to its active + // panel). `Window::focus` suppresses `refresh` while a draw is in progress, so + // schedule another frame here to render the new focus state and dispatch the + // resulting focus events. + if self.focus != focus_before_listeners { + self.refresh(); + } + self.needs_present.set(true); + + #[cfg(feature = "profiler")] + { + let draw_duration = self + .window_profiler + .end_draw(frame_dirty.dirty_at, frame_dirty.invalidations); + self.debug_frame_overlay.record_frame(draw_duration); + } + + // Exit the scope to obtain the arena-clear token this draw owes; the + // scope's teardown itself happens in `ElementArenaScope::drop`. + arena_scope.exit(&cx.element_arena) + } + + fn record_entities_accessed(&mut self, cx: &mut App) { + let mut entities_ref = cx.entities.accessed_entities.get_mut(); + let mut entities = mem::take(entities_ref.deref_mut()); + let handle = self.handle; + cx.record_entities_accessed( + handle, + // Try moving window invalidator into the Window + self.invalidator.clone(), + &entities, + ); + let mut entities_ref = cx.entities.accessed_entities.get_mut(); + mem::swap(&mut entities, entities_ref.deref_mut()); + } + + fn invalidate_entities(&mut self) { + let mut views = self.invalidator.take_views(); + for entity in views.drain() { + self.mark_view_dirty(entity); + } + self.invalidator.replace_views(views); + } + + #[profiling::function] + fn present(&mut self) { + #[cfg(feature = "profiler")] + let _foreground_turn = profiler::journal::foreground_turn(); + #[cfg(feature = "profiler")] + let present_start = Instant::now(); + self.platform_window.draw(&self.rendered_frame.scene); + #[cfg(feature = "profiler")] + self.window_profiler.record_present( + present_start, + Instant::now(), + self.active.get(), + !self.next_frame_callbacks.borrow().is_empty(), + ); + self.needs_present.set(false); + profiling::finish_frame!(); + } + + /// Presents the most recently drawn frame if it hasn't been presented yet. + #[cfg(all(test, feature = "profiler"))] + pub fn present_if_needed(&mut self) { + if self.needs_present.get() { + self.present(); + } + } + + /// Returns a snapshot of the current input-latency histograms. + #[cfg(feature = "profiler")] + pub fn input_latency_snapshot(&self) -> profiler::InputLatencySnapshot { + self.window_profiler.input_latency_snapshot() + } + + /// Returns a snapshot of the current frame-duration histograms. + #[cfg(feature = "profiler")] + pub fn frame_duration_snapshot(&self) -> profiler::FrameDurationSnapshot { + self.window_profiler.frame_duration_snapshot() + } + + /// Returns the current mode of the debug frame overlay. + #[cfg(feature = "profiler")] + pub fn debug_frame_overlay_mode(&self) -> DebugFrameOverlayMode { + self.debug_frame_overlay.mode() + } + + /// Sets the mode of the debug frame overlay and schedules a redraw. + #[cfg(feature = "profiler")] + pub fn set_debug_frame_overlay_mode(&mut self, mode: DebugFrameOverlayMode) { + self.debug_frame_overlay.set_mode(mode); + self.refresh(); + } + + /// Advances the debug frame overlay through its hidden, frame-time-only, + /// and detailed modes. + #[cfg(feature = "profiler")] + pub fn cycle_debug_frame_overlay_mode(&mut self) { + self.set_debug_frame_overlay_mode(self.debug_frame_overlay.mode().next()); + } + + /// Clears the debug frame overlay's frame-time statistics, except for the + /// total frame count, and schedules a redraw. + #[cfg(feature = "profiler")] + pub fn reset_debug_frame_overlay_stats(&mut self) { + self.debug_frame_overlay.reset_stats(); + self.refresh(); + } + + fn draw_roots(&mut self, cx: &mut App) { + self.invalidator.set_phase(DrawPhase::Prepaint); + self.tooltip_bounds.take(); + + self.a11y.sync_active_flag(); + if self.a11y.is_active() { + self.a11y.begin_frame(); + } + + let _inspector_width: Pixels = rems(30.0).to_pixels(self.rem_size()); + let root_size = { + #[cfg(any(feature = "inspector", debug_assertions))] + { + if self.inspector.is_some() { + let mut size = self.viewport_size; + size.width = (size.width - _inspector_width).max(px(0.0)); + size + } else { + self.viewport_size + } + } + #[cfg(not(any(feature = "inspector", debug_assertions)))] + { + self.viewport_size + } + }; + + // Layout all root elements. Like the root element on the web, which + // stretches to fill the viewport unless explicitly sized, window roots + // fill the window when their size is `auto`. + let scale_factor = self.scale_factor(); + let mut root_element = self.root.as_ref().unwrap().clone().into_any_element(); + let root_layout_id = root_element.request_layout(self, cx); + self.layout_engine + .as_mut() + .unwrap() + .stretch_auto_size_to_fill(root_layout_id, root_size, scale_factor); + root_element.prepaint_as_root(Point::default(), root_size.into(), self, cx); + + #[cfg(any(feature = "inspector", debug_assertions))] + let inspector_element = self.prepaint_inspector(_inspector_width, cx); + + self.prepaint_deferred_draws(cx); + + let mut prompt_element = None; + let mut active_drag_element = None; + let mut tooltip_element = None; + if let Some(prompt) = self.prompt.take() { + let mut element = prompt.view.any_view().into_any_element(); + let prompt_layout_id = element.request_layout(self, cx); + self.layout_engine + .as_mut() + .unwrap() + .stretch_auto_size_to_fill(prompt_layout_id, root_size, scale_factor); + element.prepaint_as_root(Point::default(), root_size.into(), self, cx); + prompt_element = Some(element); + self.prompt = Some(prompt); + } else if let Some(active_drag) = cx.active_drag.take() { + let mut element = active_drag.view.clone().into_any_element(); + let offset = self.mouse_position() - active_drag.cursor_offset; + element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx); + active_drag_element = Some(element); + cx.active_drag = Some(active_drag); + } else { + tooltip_element = self.prepaint_tooltip(cx); + } + + self.mouse_hit_test = self.next_frame.hit_test(self.mouse_position); + + // Now actually paint the elements. + self.invalidator.set_phase(DrawPhase::Paint); + root_element.paint(self, cx); + + #[cfg(any(feature = "inspector", debug_assertions))] + self.paint_inspector(inspector_element, cx); + + self.paint_deferred_draws(cx); + + if let Some(mut prompt_element) = prompt_element { + prompt_element.paint(self, cx); + } else if let Some(mut drag_element) = active_drag_element { + drag_element.paint(self, cx); + } else if let Some(mut tooltip_element) = tooltip_element { + tooltip_element.paint(self, cx); + } + + #[cfg(any(feature = "inspector", debug_assertions))] + self.paint_inspector_hitbox(cx); + + // a11y may have been activated/deactivated halfway through the frame + let a11y_active_start_of_frame = self.a11y.is_active(); + self.a11y.sync_active_flag(); + let a11y_active_end_of_frame = self.a11y.is_active(); + + let should_send_a11y_update = a11y_active_start_of_frame && a11y_active_end_of_frame; + + if a11y_active_start_of_frame { + // Harvest frame metadata for the debug dump while the live window + // and frame are still in scope. + let frame_info = crate::window::a11y::debug::FrameDebugInfo { + viewport_size: self.viewport_size, + scale_factor: self.scale_factor, + tab_stop_count: self.next_frame.tab_stops.tab_stop_count(), + }; + // clear the builder state regardless + let tree_update = self.a11y.end_frame(frame_info); + + if should_send_a11y_update { + log::debug!( + "Sending a11y tree update: {} nodes", + tree_update.nodes.len() + ); + self.platform_window.a11y_tree_update(tree_update); + } + } + } + + fn prepaint_tooltip(&mut self, cx: &mut App) -> Option { + // Use indexing instead of iteration to avoid borrowing self for the duration of the loop. + for tooltip_request_index in (0..self.next_frame.tooltip_requests.len()).rev() { + let Some(Some(tooltip_request)) = self + .next_frame + .tooltip_requests + .get(tooltip_request_index) + .cloned() + else { + log::error!("Unexpectedly absent TooltipRequest"); + continue; + }; + let mut element = tooltip_request.tooltip.view.clone().into_any_element(); + let mouse_position = tooltip_request.tooltip.mouse_position; + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self, cx); + + let mut tooltip_bounds = + Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); + let window_bounds = Bounds { + origin: Point::default(), + size: self.viewport_size(), + }; + + if tooltip_bounds.right() > window_bounds.right() { + let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); + if new_x >= Pixels::ZERO { + tooltip_bounds.origin.x = new_x; + } else { + tooltip_bounds.origin.x = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), + ); + } + } + + if tooltip_bounds.bottom() > window_bounds.bottom() { + let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); + if new_y >= Pixels::ZERO { + tooltip_bounds.origin.y = new_y; + } else { + tooltip_bounds.origin.y = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), + ); + } + } + + // It's possible for an element to have an active tooltip while not being painted (e.g. + // via the `visible_on_hover` method). Since mouse listeners are not active in this + // case, instead update the tooltip's visibility here. + let is_visible = + (tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self, cx); + if !is_visible { + continue; + } + + self.with_absolute_element_offset(tooltip_bounds.origin, |window| { + element.prepaint(window, cx) + }); + + self.tooltip_bounds = Some(TooltipBounds { + id: tooltip_request.id, + bounds: tooltip_bounds, + }); + return Some(element); + } + None + } + + fn prepaint_deferred_draws(&mut self, cx: &mut App) { + assert_eq!(self.element_id_stack.len(), 0); + + // Process deferred draws in multiple rounds to support nesting. + // Each round processes all current deferred draws, which may push new ones. + // + // The draws are processed in place rather than being moved out of + // `next_frame.deferred_draws`: `prepaint_index` snapshots that vector's + // length, so any prepaint range recorded during a round (view caches, + // nested deferred draws) must index the same vector `reuse_prepaint` + // slices on the next frame. Moving the draws out and re-appending them + // shifts the indices of nested draws, causing reused subtrees to graft + // the wrong deferred draws and panic in the dispatch tree. + let mut round_start = 0; + let mut depth = 0; + loop { + let round_end = self.next_frame.deferred_draws.len(); + if round_start == round_end { + break; + } + // Limit maximum nesting depth to prevent infinite loops. + assert!(depth < 10, "Exceeded maximum (10) deferred depth"); + depth += 1; + + // Sort this round by priority. + let mut traversal_order = (round_start..round_end).collect::>(); + traversal_order.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); + + for deferred_draw_ix in traversal_order { + let (element, parent_node, current_view, rem_size, absolute_offset, prepaint_range) = { + let deferred_draw = &mut self.next_frame.deferred_draws[deferred_draw_ix]; + self.element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.text_style_stack + .clone_from(&deferred_draw.text_style_stack); + ( + deferred_draw.element.take(), + deferred_draw.parent_node, + deferred_draw.current_view, + deferred_draw.rem_size, + deferred_draw.absolute_offset, + deferred_draw.prepaint_range.clone(), + ) + }; + self.next_frame.dispatch_tree.set_active_node(parent_node); + + let prepaint_start = self.prepaint_index(); + if let Some(mut element) = element { + self.with_rendered_view(current_view, |window| { + window.with_rem_size(Some(rem_size), |window| { + window.with_absolute_element_offset(absolute_offset, |window| { + element.prepaint(window, cx); + }); + }); + }); + self.next_frame.deferred_draws[deferred_draw_ix].element = Some(element); + } else { + self.reuse_prepaint(prepaint_range); + } + let prepaint_end = self.prepaint_index(); + self.next_frame.deferred_draws[deferred_draw_ix].prepaint_range = + prepaint_start..prepaint_end; + } + + self.element_id_stack.clear(); + self.text_style_stack.clear(); + round_start = round_end; + } + } + + fn paint_deferred_draws(&mut self, cx: &mut App) { + assert_eq!(self.element_id_stack.len(), 0); + + // Paint all deferred draws in priority order. + // Since prepaint has already processed nested deferreds, we just paint them all. + if self.next_frame.deferred_draws.len() == 0 { + return; + } + + let traversal_order = self.deferred_draw_traversal_order(); + let mut deferred_draws = mem::take(&mut self.next_frame.deferred_draws); + for deferred_draw_ix in traversal_order { + let mut deferred_draw = &mut deferred_draws[deferred_draw_ix]; + self.element_id_stack + .clone_from(&deferred_draw.element_id_stack); + self.next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let paint_start = self.paint_index(); + let content_mask = deferred_draw.content_mask; + if let Some(element) = deferred_draw.element.as_mut() { + self.with_rendered_view(deferred_draw.current_view, |window| { + window.with_content_mask(content_mask, |window| { + window.with_rem_size(Some(deferred_draw.rem_size), |window| { + element.paint(window, cx); + }); + }) + }) + } else { + self.reuse_paint(deferred_draw.paint_range.clone()); + } + let paint_end = self.paint_index(); + deferred_draw.paint_range = paint_start..paint_end; + } + self.next_frame.deferred_draws = deferred_draws; + self.element_id_stack.clear(); + } + + fn deferred_draw_traversal_order(&mut self) -> SmallVec<[usize; 8]> { + let deferred_count = self.next_frame.deferred_draws.len(); + let mut sorted_indices = (0..deferred_count).collect::>(); + sorted_indices.sort_by_key(|ix| self.next_frame.deferred_draws[*ix].priority); + sorted_indices + } + + pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { + PrepaintStateIndex { + hitboxes_index: self.next_frame.hitboxes.len(), + tooltips_index: self.next_frame.tooltip_requests.len(), + deferred_draws_index: self.next_frame.deferred_draws.len(), + dispatch_tree_index: self.next_frame.dispatch_tree.len(), + accessed_element_states_index: self.next_frame.accessed_element_states.len(), + line_layout_index: self.text_system.layout_index(), + } + } + + pub(crate) fn reuse_prepaint(&mut self, range: Range) { + self.next_frame.hitboxes.extend( + self.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] + .iter() + .cloned(), + ); + self.next_frame.tooltip_requests.extend( + self.rendered_frame.tooltip_requests + [range.start.tooltips_index..range.end.tooltips_index] + .iter_mut() + .map(|request| request.take()), + ); + self.next_frame.accessed_element_states.extend( + self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (id.clone(), *type_id)), + ); + self.text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + + let reused_subtree = self.next_frame.dispatch_tree.reuse_subtree( + range.start.dispatch_tree_index..range.end.dispatch_tree_index, + &mut self.rendered_frame.dispatch_tree, + self.focus, + ); + + if reused_subtree.contains_focus() { + self.next_frame.focus = self.focus; + } + + self.next_frame.deferred_draws.extend( + self.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + current_view: deferred_draw.current_view, + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + content_mask: deferred_draw.content_mask, + rem_size: deferred_draw.rem_size, + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + prepaint_range: deferred_draw.prepaint_range.clone(), + paint_range: deferred_draw.paint_range.clone(), + }), + ); + } + + pub(crate) fn paint_index(&self) -> PaintIndex { + PaintIndex { + scene_index: self.next_frame.scene.len(), + #[cfg(any(test, feature = "test-support"))] + debug_bounds_index: self.next_frame.debug_bounds_records.len(), + mouse_listeners_index: self.next_frame.mouse_listeners.len(), + input_handlers_index: self.next_frame.input_handlers.len(), + cursor_styles_index: self.next_frame.cursor_styles.len(), + accessed_element_states_index: self.next_frame.accessed_element_states.len(), + tab_handle_index: self.next_frame.tab_stops.paint_index(), + line_layout_index: self.text_system.layout_index(), + } + } + + pub(crate) fn reuse_paint(&mut self, range: Range) { + // Cached elements still exist in the frame even when their paint methods don't run. + #[cfg(any(test, feature = "test-support"))] + for (selector, bounds) in &self.rendered_frame.debug_bounds_records + [range.start.debug_bounds_index..range.end.debug_bounds_index] + { + self.next_frame + .record_debug_bounds(selector.clone(), *bounds); + } + self.next_frame.cursor_styles.extend( + self.rendered_frame.cursor_styles + [range.start.cursor_styles_index..range.end.cursor_styles_index] + .iter() + .cloned(), + ); + self.next_frame.input_handlers.extend( + self.rendered_frame.input_handlers + [range.start.input_handlers_index..range.end.input_handlers_index] + .iter_mut() + .map(|handler| handler.take()), + ); + self.next_frame.mouse_listeners.extend( + self.rendered_frame.mouse_listeners + [range.start.mouse_listeners_index..range.end.mouse_listeners_index] + .iter_mut() + .map(|listener| listener.take()), + ); + self.next_frame.accessed_element_states.extend( + self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index + ..range.end.accessed_element_states_index] + .iter() + .map(|(id, type_id)| (id.clone(), *type_id)), + ); + self.next_frame.tab_stops.replay( + &self.rendered_frame.tab_stops.insertion_history + [range.start.tab_handle_index..range.end.tab_handle_index], + ); + + self.text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + self.next_frame.scene.replay( + range.start.scene_index..range.end.scene_index, + &self.rendered_frame.scene, + ); + } + + /// Push a text style onto the stack, and call a function with that style active. + /// Use [`Window::text_style`] to get the current, combined text style. This method + /// should only be called as part of element drawing. + pub fn with_text_style(&mut self, style: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.invalidator.debug_assert_paint_or_prepaint(); + if let Some(style) = style { + self.text_style_stack.push(style); + let result = f(self); + self.text_style_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the cursor style at the platform level. This method should only be called + /// during the paint phase of element drawing. + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { + self.invalidator.debug_assert_paint(); + self.next_frame.cursor_styles.push(CursorStyleRequest { + hitbox_id: Some(hitbox.id), + style, + }); + } + + /// Updates the cursor style for the entire window at the platform level. A cursor + /// style using this method will have precedence over any cursor style set using + /// `set_cursor_style`. This method should only be called during the paint + /// phase of element drawing. + pub fn set_window_cursor_style(&mut self, style: CursorStyle) { + self.invalidator.debug_assert_paint(); + self.next_frame.cursor_styles.push(CursorStyleRequest { + hitbox_id: None, + style, + }) + } + + /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called + /// during the paint phase of element drawing. + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { + self.invalidator.debug_assert_prepaint(); + let id = TooltipId(post_inc(&mut self.next_tooltip_id.0)); + self.next_frame + .tooltip_requests + .push(Some(TooltipRequest { id, tooltip })); + id + } + + /// Invoke the given function with the given content mask after intersecting it + /// with the current mask. This method should only be called during element drawing. + // This function is called in a highly recursive manner in editor + // prepainting, make sure its inlined to reduce the stack burden + #[inline] + pub fn with_content_mask( + &mut self, + mask: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_paint_or_prepaint(); + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.content_mask_stack.push(mask); + let result = f(self); + self.content_mask_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the global element offset relative to the current offset. This is used to implement + /// scrolling. This method should only be called during the prepaint phase of element drawing. + pub fn with_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_prepaint(); + + if offset.is_zero() { + return f(self); + }; + + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Updates the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. This method should only be called during + /// the prepaint phase of element drawing. + pub fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_prepaint(); + self.element_offset_stack.push(offset); + let result = f(self); + self.element_offset_stack.pop(); + result + } + + pub(crate) fn with_element_opacity( + &mut self, + opacity: Option, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.invalidator.debug_assert_paint_or_prepaint(); + + let Some(opacity) = opacity else { + return f(self); + }; + + let previous_opacity = self.element_opacity; + self.element_opacity = previous_opacity * opacity; + let result = f(self); + self.element_opacity = previous_opacity; + result + } + + /// Perform prepaint on child elements in a "retryable" manner, so that any side effects + /// of prepaints can be discarded before prepainting again. This is used to support autoscroll + /// where we need to prepaint children to detect the autoscroll bounds, then adjust the + /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be + /// called during the prepaint phase of element drawing. + pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + self.invalidator.debug_assert_prepaint(); + let index = self.prepaint_index(); + let result = f(self); + if result.is_err() { + self.next_frame.hitboxes.truncate(index.hitboxes_index); + self.next_frame + .tooltip_requests + .truncate(index.tooltips_index); + self.next_frame + .deferred_draws + .truncate(index.deferred_draws_index); + self.next_frame + .dispatch_tree + .truncate(index.dispatch_tree_index); + self.next_frame + .accessed_element_states + .truncate(index.accessed_element_states_index); + self.text_system.truncate_layouts(index.line_layout_index); + } + result + } + + /// When you call this method during [`Element::prepaint`], containing elements will attempt to + /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call + /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element + /// that supports this method being called on the elements it contains. This method should only be + /// called during the prepaint phase of element drawing. + pub fn request_autoscroll(&mut self, bounds: Bounds) { + self.invalidator.debug_assert_prepaint(); + self.requested_autoscroll = Some(bounds); + } + + /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior + /// described in [`Self::request_autoscroll`]. + pub fn take_autoscroll(&mut self) -> Option> { + self.invalidator.debug_assert_prepaint(); + self.requested_autoscroll.take() + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + pub fn use_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { + cx.asset_entry::(source).use_by(self.current_view()) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading or doesn't exist this will return None. + /// Your view will not be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { + cx.fetch_asset::(source) + } + /// Obtain the current element offset. This method should only be called during the + /// prepaint phase of element drawing. + pub fn element_offset(&self) -> Point { + self.invalidator.debug_assert_prepaint(); + self.element_offset_stack + .last() + .copied() + .unwrap_or_default() + } + + /// Obtain the current element opacity. This method should only be called during the + /// prepaint phase of element drawing. + #[inline] + pub(crate) fn element_opacity(&self) -> f32 { + self.invalidator.debug_assert_paint_or_prepaint(); + self.element_opacity + } + + /// Obtain the current content mask. This method should only be called during element drawing. + pub fn content_mask(&self) -> ContentMask { + self.invalidator.debug_assert_paint_or_prepaint(); + self.content_mask_stack + .last() + .cloned() + .unwrap_or_else(|| ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.viewport_size, + }, + }) + } + + /// Provide elements in the called function with a new namespace in which their identifiers must be unique. + /// This can be used within a custom element to distinguish multiple sets of child elements. + pub fn with_element_namespace( + &mut self, + element_id: impl Into, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.element_id_stack.push(element_id.into()); + let result = f(self); + self.element_id_stack.pop(); + result + } + + /// Use a piece of state that exists as long this element is being rendered in consecutive frames. + pub fn use_keyed_state( + &mut self, + key: impl Into, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut Context) -> S, + ) -> Entity { + let current_view = self.current_view(); + self.with_global_id(key.into(), |global_id, window| { + window.with_element_state(global_id, |state: Option>, window| { + if let Some(state) = state { + (state.clone(), state) + } else { + let new_state = cx.new(|cx| init(window, cx)); + Self::observe_keyed_state(&new_state, current_view, cx); + (new_state.clone(), new_state) + } + }) + }) + } + + /// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key + /// + /// NOTE: This method uses the location of the caller to generate an ID for this state. + /// If this is not sufficient to identify your state (e.g. you're rendering a list item), + /// you can provide a custom ElementID using the `use_keyed_state` method. + #[track_caller] + pub fn use_state( + &mut self, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut Context) -> S, + ) -> Entity { + self.use_keyed_state( + ElementId::CodeLocation(*core::panic::Location::caller()), + cx, + init, + ) + } + + /// Updates or initializes state for an element with the given id that lives across multiple + /// frames. If an element with this ID existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. This method should only be called as part of element drawing. + #[inline(always)] + pub fn with_element_state( + &mut self, + global_id: &GlobalElementId, + f: impl FnOnce(Option, &mut Self) -> (R, S), + ) -> R + where + S: 'static, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + let (key, state) = self.take_element_state(global_id, TypeId::of::()); + + if let Some(any) = state { + let ElementStateBox { + inner, + #[cfg(debug_assertions)] + type_name, + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}, actual: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow::anyhow!( + "invalid element state type for id, requested {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + let state = state_box.take().expect( + "reentrant call to with_element_state for the same state type and element id", + ); + let (result, state) = f(Some(state), self); + state_box.replace(state); + self.insert_element_state( + key, + ElementStateBox { + inner: state_box, + #[cfg(debug_assertions)] + type_name, + }, + ); + result + } else { + let (result, state) = f(None, self); + self.insert_element_state( + key, + ElementStateBox { + inner: Box::new(Some(state)), + #[cfg(debug_assertions)] + type_name: std::any::type_name::(), + }, + ); + result + } + } + + /// A variant of `with_element_state` that allows the element's id to be optional. This is a convenience + /// method for elements where the element id may or may not be assigned. Prefer using `with_element_state` + /// when the element is guaranteed to have an id. + /// + /// The first option means 'no ID provided' + /// The second option means 'not yet initialized' + pub fn with_optional_element_state( + &mut self, + global_id: Option<&GlobalElementId>, + f: impl FnOnce(Option>, &mut Self) -> (R, Option), + ) -> R + where + S: 'static, + { + self.invalidator.debug_assert_paint_or_prepaint(); + + if let Some(global_id) = global_id { + self.with_element_state(global_id, |state, cx| { + let (result, state) = f(Some(state), cx); + let state = + state.expect("you must return some state when you pass some element id"); + (result, state) + }) + } else { + let (result, state) = f(None, self); + debug_assert!( + state.is_none(), + "you must not return an element state when passing None for the global id" + ); + result + } + } + + /// Executes the given closure within the context of a tab group. + #[inline] + pub fn with_tab_group(&mut self, index: Option, f: impl FnOnce(&mut Self) -> R) -> R { + if let Some(index) = index { + self.next_frame.tab_stops.begin_group(index); + let result = f(self); + self.next_frame.tab_stops.end_group(); + result + } else { + f(self) + } + } + + /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree + /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + /// + /// When `content_mask` is provided, the deferred element will be clipped to that region during + /// both prepaint and paint. When `None`, no additional clipping is applied. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn defer_draw( + &mut self, + element: AnyElement, + absolute_offset: Point, + priority: usize, + content_mask: Option>, + ) { + self.invalidator.debug_assert_prepaint(); + let parent_node = self.next_frame.dispatch_tree.active_node_id().unwrap(); + self.next_frame.deferred_draws.push(DeferredDraw { + current_view: self.current_view(), + parent_node, + element_id_stack: self.element_id_stack.clone(), + text_style_stack: self.text_style_stack.clone(), + content_mask, + rem_size: self.rem_size(), + priority, + element: Some(element), + absolute_offset, + prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), + paint_range: PaintIndex::default()..PaintIndex::default(), + }); + } + + /// Creates a new painting layer for the specified bounds. A "layer" is a batch + /// of geometry that are non-overlapping and have the same draw order. This is typically used + /// for performance reasons. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { + self.invalidator.debug_assert_paint(); + + let content_mask = self.content_mask(); + let clipped_bounds = bounds.intersect(&content_mask.bounds); + if !clipped_bounds.is_empty() { + self.next_frame + .scene + .push_layer(self.cover_bounds(clipped_bounds)); + } + + let result = f(self); + + if !clipped_bounds.is_empty() { + self.next_frame.scene.pop_layer(); + } + + result + } + + /// Paint the drop (non-inset) shadows from `shadows` into the scene at the current + /// z-index. Inset shadows are skipped; paint those with [`Self::paint_inset_shadows`] + /// after the element's background so they layer on top of the fill. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_drop_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.snapped_content_mask(); + let opacity = self.element_opacity(); + let element_bounds = self.cover_bounds(bounds); + let element_corner_radii = corner_radii.scale(scale_factor); + for shadow in shadows { + if shadow.inset { + continue; + } + let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius); + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(shadow_bounds), + content_mask, + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + element_bounds, + element_corner_radii, + inset: 0, + pad: 0, + }); + } + } + + /// Paint the inset shadows from `shadows` into the scene at the current z-index. Should + /// be called after the element's background so the shadow layers on top of the fill. + /// Drop shadows are skipped; paint those with [`Self::paint_drop_shadows`] before the background. + pub fn paint_inset_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.snapped_content_mask(); + let opacity = self.element_opacity(); + let element_bounds = self.cover_bounds(bounds); + let element_corner_radii = corner_radii.scale(scale_factor); + for shadow in shadows { + if !shadow.inset { + continue; + } + let hole = (bounds + shadow.offset).dilate(-shadow.spread_radius); + // Clamp at zero so a large spread can't produce negative radii, which would + // break the SDF in the shader. + let zero = Pixels::ZERO; + let hole_corner_radii = Corners { + top_left: (corner_radii.top_left - shadow.spread_radius).max(zero), + top_right: (corner_radii.top_right - shadow.spread_radius).max(zero), + bottom_right: (corner_radii.bottom_right - shadow.spread_radius).max(zero), + bottom_left: (corner_radii.bottom_left - shadow.spread_radius).max(zero), + }; + self.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: self.cover_bounds(hole), + content_mask, + corner_radii: hole_corner_radii.scale(scale_factor), + color: shadow.color.opacity(opacity), + element_bounds, + element_corner_radii, + inset: 1, + pad: 0, + }); + } + } + + fn largest_border_interior(quad: &Quad) -> Bounds { + let radii = &quad.corner_radii; + let widths = &quad.border_widths; + let edge_radii = Edges { + top: radii.top_left.max(radii.top_right), + right: radii.top_right.max(radii.bottom_right), + bottom: radii.bottom_left.max(radii.bottom_right), + left: radii.top_left.max(radii.bottom_left), + }; + + let antialias_inset = point(ScaledPixels(1.0), ScaledPixels(1.0)); + let inset_bounds = |top_left_inset, bottom_right_inset| { + Bounds::from_corners( + quad.bounds.origin + top_left_inset + antialias_inset, + quad.bounds.bottom_right() - bottom_right_inset - antialias_inset, + ) + }; + + // Rounded corners need only be excluded on one axis. Either candidate + // is empty of border pixels, so use the larger interior. + let horizontal_band = inset_bounds( + point(widths.left, widths.top.max(edge_radii.top)), + point(widths.right, widths.bottom.max(edge_radii.bottom)), + ); + let vertical_band = inset_bounds( + point(widths.left.max(edge_radii.left), widths.top), + point(widths.right.max(edge_radii.right), widths.bottom), + ); + + let area = |bounds: &Bounds| { + bounds.size.width.0.max(0.) * bounds.size.height.0.max(0.) + }; + if area(&horizontal_band) >= area(&vertical_band) { + horizontal_band + } else { + vertical_band + } + } + + /// Paint one or more quads into the scene for the next frame at the current stacking context. + /// Quads are colored rectangular regions with an optional background, border, and corner radius. + /// see [`fill`], [`outline`], and [`quad`] to construct this type. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// Note that the `quad.corner_radii` are allowed to exceed the bounds, creating sharp corners + /// where the circular arcs meet. This will not display well when combined with dashed borders. + /// Use `Corners::clamp_radii_for_quad_size` if the radii should fit within the bounds. + pub fn paint_quad(&mut self, quad: PaintQuad) { + self.invalidator.debug_assert_paint(); + + let opacity = self.element_opacity(); + let snapped_bounds = self.snap_bounds(quad.bounds); + let snapped_border_widths = self.snap_border_widths(quad.border_widths); + let quad = Quad { + order: 0, + bounds: snapped_bounds, + content_mask: self.snapped_content_mask(), + background: quad.background.opacity(opacity), + border_color: quad.border_color.opacity(opacity), + corner_radii: quad.corner_radii.scale(self.scale_factor()), + border_widths: snapped_border_widths, + border_style: quad.border_style, + }; + + if !quad.background.is_transparent() { + self.next_frame.scene.insert_primitive(quad); + return; + } + + // Splitting a border-only quad around its empty interior avoids shading + // every transparent pixel inside large outlines. + let outer_bounds = quad.bounds; + let inner_bounds = Self::largest_border_interior(&quad); + + if inner_bounds.is_empty() { + self.next_frame.scene.insert_primitive(quad); + return; + } + + let strips = [ + // Top + Bounds::from_corners( + outer_bounds.origin, + point(outer_bounds.right(), inner_bounds.top()), + ), + // Bottom + Bounds::from_corners( + point(outer_bounds.left(), inner_bounds.bottom()), + outer_bounds.bottom_right(), + ), + // Left + Bounds::from_corners( + point(outer_bounds.left(), inner_bounds.top()), + inner_bounds.bottom_left(), + ), + // Right + Bounds::from_corners( + inner_bounds.top_right(), + point(outer_bounds.right(), inner_bounds.bottom()), + ), + ]; + + for strip in strips { + let content_mask_bounds = quad.content_mask.bounds.intersect(&strip); + if !content_mask_bounds.is_empty() { + self.next_frame.scene.insert_primitive(Quad { + content_mask: ContentMask { + bounds: content_mask_bounds, + }, + ..quad + }); + } + } + } + + /// Paint the given `Path` into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let opacity = self.element_opacity(); + path.content_mask = content_mask; + let color: Background = color.into(); + path.color = color.opacity(opacity); + self.next_frame + .scene + .insert_primitive(path.scale(scale_factor)); + } + + /// Paint an underline into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_underline( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) { + self.invalidator.debug_assert_paint(); + let underline = self.underline(origin, width, style); + self.next_frame.scene.insert_primitive(underline); + } + + /// Paint a strikethrough into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_strikethrough( + &mut self, + origin: Point, + width: Pixels, + style: &StrikethroughStyle, + ) { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let height = style.thickness; + let bounds = Bounds { + origin: origin.map(|c| ScaledPixels(round_to_device_pixel(c.0, scale_factor))), + size: size(self.snap_stroke(width), self.snap_stroke(height)), + }; + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds, + content_mask: self.snapped_content_mask(), + thickness: self.snap_stroke(style.thickness), + color: style.color.unwrap_or_default().opacity(opacity), + wavy: false.into(), + }); + } + + /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single glyph that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_glyph( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + color: Hsla, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let element_opacity = self.element_opacity(); + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + + let quantized_origin = Point::new( + round_half_toward_zero(glyph_origin.x.0 * SUBPIXEL_VARIANTS_X as f32) + / SUBPIXEL_VARIANTS_X as f32, + round_half_toward_zero(glyph_origin.y.0 * SUBPIXEL_VARIANTS_Y as f32) + / SUBPIXEL_VARIANTS_Y as f32, + ); + let subpixel_variant = Point::new( + (quantized_origin.x.fract() * SUBPIXEL_VARIANTS_X as f32) as u8, + (quantized_origin.y.fract() * SUBPIXEL_VARIANTS_Y as f32) as u8, + ); + let integer_origin = quantized_origin.map(|c| ScaledPixels(c.trunc())); + let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size); + let dilation = self.text_system().glyph_dilation_for_color(color); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant, + scale_factor, + is_emoji: false, + subpixel_rendering, + dilation, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = self + .sprite_atlas + .get_or_insert_with(params.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok(Some((size, Cow::Owned(bytes)))) + })? + .expect("Callback above only errors or returns Some"); + let bounds = Bounds { + origin: integer_origin + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.snapped_content_mask(); + + if subpixel_rendering { + self.next_frame.scene.insert_primitive(SubpixelSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color: color.opacity(element_opacity), + tile, + transformation: TransformationMatrix::unit(), + }); + } else { + self.next_frame.scene.insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color: color.opacity(element_opacity), + tile, + transformation: TransformationMatrix::unit(), + }); + } + } + Ok(()) + } + + fn should_use_subpixel_rendering(&self, font_id: FontId, font_size: Pixels) -> bool { + if self.platform_window.background_appearance() != WindowBackgroundAppearance::Opaque { + return false; + } + + if !self.platform_window.is_subpixel_rendering_supported() { + return false; + } + + let mode = match self.text_rendering_mode.get() { + TextRenderingMode::PlatformDefault => self + .text_system() + .recommended_rendering_mode(font_id, font_size), + mode => mode, + }; + + mode == TextRenderingMode::Subpixel + } + + /// Paints an emoji glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single emoji that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_emoji( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let integer_origin = glyph_origin.map(|c| ScaledPixels(round_half_toward_zero(c.0))); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant: Default::default(), + scale_factor, + is_emoji: true, + subpixel_rendering: false, + dilation: 0, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = self + .sprite_atlas + .get_or_insert_with(params.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok(Some((size, Cow::Owned(bytes)))) + })? + .expect("Callback above only errors or returns Some"); + + let bounds = Bounds { + origin: integer_origin + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.snapped_content_mask(); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(PolychromeSprite { + order: 0, + pad: 0, + grayscale: false.into(), + bounds, + corner_radii: Default::default(), + content_mask, + tile, + opacity, + }); + } + Ok(()) + } + + /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_svg( + &mut self, + bounds: Bounds, + path: SharedString, + mut data: Option<&[u8]>, + transformation: TransformationMatrix, + color: Hsla, + cx: &App, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let element_opacity = self.element_opacity(); + let bounds = self.snap_bounds(bounds); + + let params = RenderSvgParams { + path, + size: bounds.size.map(|pixels| { + DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32) + }), + }; + + let Some(tile) = + self.sprite_atlas + .get_or_insert_with(params.clone().into(), &mut || { + let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)? + else { + return Ok(None); + }; + Ok(Some((size, Cow::Owned(bytes)))) + })? + else { + return Ok(()); + }; + let content_mask = self.snapped_content_mask(); + let svg_bounds = Bounds { + origin: bounds.center() + - Point::new( + ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.), + ), + size: tile + .bounds + .size + .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)), + }; + let final_bounds = svg_bounds + .map_origin(|value| ScaledPixels(round_half_toward_zero(value.0))) + .map_size(|size| size.ceil()); + + self.next_frame.scene.insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds: final_bounds, + content_mask, + color: color.opacity(element_opacity), + tile, + transformation, + }); + + Ok(()) + } + + /// Paint an image into the scene for the next frame at the current z-index. + /// This method will panic if the frame_index is not valid + /// + /// This method should only be called as part of the paint phase of element drawing. + /// Paint an image into `bounds`, positioning and scaling it according to `image_bounds`. + /// + /// The visible region rendered is `bounds.intersect(&image_bounds)`, with `corner_radii` + /// applied to `bounds`. + pub fn paint_image( + &mut self, + bounds: Bounds, + image_bounds: Bounds, + corner_radii: Corners, + data: Arc, + frame_index: usize, + grayscale: bool, + ) -> Result<()> { + self.invalidator.debug_assert_paint(); + + let visible_bounds = bounds.intersect(&image_bounds); + if visible_bounds.size.width <= Pixels::ZERO || visible_bounds.size.height <= Pixels::ZERO { + return Ok(()); + } + if image_bounds.size.width <= Pixels::ZERO || image_bounds.size.height <= Pixels::ZERO { + return Ok(()); + } + + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + let tile = self + .sprite_atlas + .get_or_insert_with(params.into(), &mut || { + Ok(Some(( + data.size(frame_index), + Cow::Borrowed( + data.as_bytes(frame_index) + .expect("It's the caller's job to pass a valid frame index"), + ), + ))) + })? + .expect("Callback above only returns Some"); + + let visible_bounds_snapped = self.snap_bounds(visible_bounds); + + let sub_tile = if visible_bounds == image_bounds { + tile + } else { + let x_offset_ratio = + (visible_bounds.origin.x - image_bounds.origin.x) / image_bounds.size.width; + let y_offset_ratio = + (visible_bounds.origin.y - image_bounds.origin.y) / image_bounds.size.height; + let width_ratio = visible_bounds.size.width / image_bounds.size.width; + let height_ratio = visible_bounds.size.height / image_bounds.size.height; + + let tile_origin_x = tile.bounds.origin.x.0; + let tile_origin_y = tile.bounds.origin.y.0; + let tile_width = tile.bounds.size.width.0; + let tile_height = tile.bounds.size.height.0; + + let sub_origin_x = tile_origin_x + (x_offset_ratio * tile_width as f32).round() as i32; + let sub_origin_y = tile_origin_y + (y_offset_ratio * tile_height as f32).round() as i32; + let sub_width = (width_ratio * tile_width as f32).round() as i32; + let sub_height = (height_ratio * tile_height as f32).round() as i32; + + let max_x = tile_origin_x + tile_width; + let max_y = tile_origin_y + tile_height; + + let clamped_origin_x = sub_origin_x.clamp(tile_origin_x, max_x); + let clamped_origin_y = sub_origin_y.clamp(tile_origin_y, max_y); + let clamped_width = sub_width.min(max_x - clamped_origin_x).max(0); + let clamped_height = sub_height.min(max_y - clamped_origin_y).max(0); + + AtlasTile { + bounds: Bounds { + origin: point( + DevicePixels(clamped_origin_x), + DevicePixels(clamped_origin_y), + ), + size: size(DevicePixels(clamped_width), DevicePixels(clamped_height)), + }, + ..tile + } + }; + + let content_mask = self.snapped_content_mask(); + let corner_radii = corner_radii + .clamp_radii_for_quad_size(visible_bounds.size) + .scale(self.scale_factor()); + let opacity = self.element_opacity(); + + self.next_frame.scene.insert_primitive(PolychromeSprite { + order: 0, + pad: 0, + grayscale: grayscale.into(), + bounds: visible_bounds_snapped, + content_mask, + corner_radii, + tile: sub_tile, + opacity, + }); + Ok(()) + } + + /// Paint a surface into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVPixelBuffer) { + use crate::PaintSurface; + + self.invalidator.debug_assert_paint(); + + let bounds = self.snap_bounds(bounds); + let content_mask = self.snapped_content_mask(); + self.next_frame.scene.insert_primitive(PaintSurface { + order: 0, + bounds, + content_mask, + image_buffer, + }); + } + + /// Removes an image from the sprite atlas. + pub fn drop_image(&mut self, data: Arc) -> Result<()> { + for frame_index in 0..data.frame_count() { + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + self.sprite_atlas.remove(¶ms.clone().into()); + } + + Ok(()) + } + + /// Returns whether every frame of an image is present in the sprite atlas. + #[cfg(any(test, feature = "test-support"))] + pub fn has_image_atlas_entry(&self, data: &RenderImage) -> bool { + data.frame_count() > 0 + && (0..data.frame_count()).all(|frame_index| { + self.sprite_atlas.contains( + &RenderImageParams { + image_id: data.id, + frame_index, + } + .into(), + ) + }) + } + + /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which + /// layout is being requested, along with the layout ids of any children. This method is called during + /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + #[must_use] + pub fn request_layout( + &mut self, + style: Style, + children: impl IntoIterator, + cx: &mut App, + ) -> LayoutId { + self.invalidator.debug_assert_prepaint(); + + cx.layout_id_buffer.clear(); + cx.layout_id_buffer.extend(children); + let rem_size = self.rem_size(); + let scale_factor = self.scale_factor(); + + self.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + scale_factor, + &cx.layout_id_buffer, + ) + } + + /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, + /// this variant takes a function that is invoked during layout so you can use arbitrary logic to + /// determine the element's size. One place this is used internally is when measuring text. + /// + /// The given closure is invoked at layout time with the known dimensions and available space and + /// returns a `Size`. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_measured_layout(&mut self, style: Style, measure: F) -> LayoutId + where + F: Fn(Size>, Size, &mut Window, &mut App) -> Size + + 'static, + { + self.invalidator.debug_assert_prepaint(); + + let rem_size = self.rem_size(); + let scale_factor = self.scale_factor(); + self.layout_engine + .as_mut() + .unwrap() + .request_measured_layout(style, rem_size, scale_factor, measure) + } + + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn compute_layout( + &mut self, + layout_id: LayoutId, + available_space: Size, + cx: &mut App, + ) { + self.invalidator.debug_assert_prepaint(); + + let mut layout_engine = self.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self, cx); + self.layout_engine = Some(layout_engine); + } + + /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by + /// GPUI itself automatically in order to pass your element its `Bounds` automatically. + /// + /// This method should only be called as part of element drawing. + pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { + self.invalidator.debug_assert_prepaint(); + + let scale_factor = self.scale_factor(); + let mut bounds = self + .layout_engine + .as_mut() + .unwrap() + .layout_bounds(layout_id, scale_factor) + .map(Into::into); + let snapped_offset = self.pixel_snap_point(self.element_offset()); + bounds.origin += snapped_offset; + bounds + } + + /// This method should be called during `prepaint`. You can use + /// the returned [Hitbox] during `paint` or in an event handler + /// to determine whether the inserted hitbox was the topmost. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn insert_hitbox(&mut self, bounds: Bounds, behavior: HitboxBehavior) -> Hitbox { + self.invalidator.debug_assert_prepaint(); + + let content_mask = self.content_mask(); + let mut id = self.next_hitbox_id; + self.next_hitbox_id = self.next_hitbox_id.next(); + let hitbox = Hitbox { + id, + bounds, + content_mask, + behavior, + }; + self.next_frame.hitboxes.push(hitbox.clone()); + hitbox + } + + /// Set a hitbox which will act as a control area of the platform window. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn insert_window_control_hitbox(&mut self, area: WindowControlArea, hitbox: Hitbox) { + self.invalidator.debug_assert_paint(); + self.next_frame.window_control_hitboxes.push((area, hitbox)); + } + + /// Sets the key context for the current element. This context will be used to translate + /// keybindings into actions. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_key_context(&mut self, context: KeyContext) { + self.invalidator.debug_assert_paint(); + self.next_frame.dispatch_tree.set_key_context(context); + } + + /// Sets the focus handle for the current element. This handle will be used to manage focus state + /// and keyboard event dispatch for the element. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle, _: &App) { + self.invalidator.debug_assert_prepaint(); + if focus_handle.is_focused(self) { + self.next_frame.focus = Some(focus_handle.id); + } + self.next_frame.dispatch_tree.set_focus_id(focus_handle.id); + } + + /// Sets the view id for the current element, which will be used to manage view caching. + /// + /// This method should only be called as part of element prepaint. We plan on removing this + /// method eventually when we solve some issues that require us to construct editor elements + /// directly instead of always using editors via views. + pub fn set_view_id(&mut self, view_id: EntityId) { + self.invalidator.debug_assert_prepaint(); + self.next_frame.dispatch_tree.set_view_id(view_id); + } + + /// Get the entity ID for the currently rendering view + pub fn current_view(&self) -> EntityId { + self.invalidator.debug_assert_paint_or_prepaint(); + self.rendered_entity_stack.last().copied().unwrap() + } + + #[inline] + pub(crate) fn with_rendered_view( + &mut self, + id: EntityId, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.rendered_entity_stack.push(id); + let result = f(self); + self.rendered_entity_stack.pop(); + result + } + + /// Executes the provided function with the specified image cache. + pub fn with_image_cache(&mut self, image_cache: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + if let Some(image_cache) = image_cache { + self.image_cache_stack.push(image_cache); + let result = f(self); + self.image_cache_stack.pop(); + result + } else { + f(self) + } + } + + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// [element_input_handler]: crate::ElementInputHandler + pub fn handle_input( + &mut self, + focus_handle: &FocusHandle, + input_handler: impl InputHandler, + cx: &App, + ) { + self.invalidator.debug_assert_paint(); + + if focus_handle.is_focused(self) { + let cx = self.to_async(cx); + self.next_frame + .input_handlers + .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); + } + } + + /// Forwards the focused input handler's [`TextInputConfiguration`] to the + /// platform window when it differs from the last forwarded value. With no + /// input handler the default configuration applies, so a field's + /// preferences don't outlive its focus. + fn apply_text_input_configuration(&mut self, cx: &mut App) { + let configuration = match self.platform_window.take_input_handler() { + Some(mut input_handler) => { + let configuration = input_handler.text_input_configuration(self, cx); + self.platform_window.set_input_handler(input_handler); + configuration + } + None => TextInputConfiguration::default(), + }; + if self.last_text_input_configuration.as_ref() != Some(&configuration) { + self.platform_window + .set_text_input_configuration(configuration.clone()); + self.last_text_input_configuration = Some(configuration); + } + } + + /// Register a mouse event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_mouse_event( + &mut self, + mut listener: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.mouse_listeners.push(Some(Box::new( + move |event: &dyn Any, phase: DispatchPhase, window: &mut Window, cx: &mut App| { + if let Some(event) = event.downcast_ref() { + listener(event, phase, window, cx) + } + }, + ))); + } + + /// Register a key event listener on this node for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a listener yourself. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_key_event( + &mut self, + listener: impl Fn(&Event, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, window: &mut Window, cx: &mut App| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, window, cx) + } + }, + )); + } + + /// Register a modifiers changed event listener on the window for the next frame. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame.dispatch_tree.on_modifiers_changed(Rc::new( + move |event: &ModifiersChangedEvent, window: &mut Window, cx: &mut App| { + listener(event, window, cx) + }, + )); + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// This does not fire if the given focus handle - or one of its descendants - was previously focused. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + cx: &mut App, + mut listener: impl FnMut(&mut Window, &mut App) + 'static, + ) -> Subscription { + let focus_id = handle.id; + let (subscription, activate) = + self.new_focus_listener(Box::new(move |event, window, cx| { + if event.is_focus_in(focus_id) { + listener(window, cx); + } + true + })); + cx.defer(move |_| activate()); + subscription + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Returns a subscription and persists until the subscription is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + cx: &mut App, + mut listener: impl FnMut(FocusOutEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + let focus_id = handle.id; + let (subscription, activate) = + self.new_focus_listener(Box::new(move |event, window, cx| { + if let Some(blurred_id) = event.previous_focus_path.last().copied() + && event.is_focus_out(focus_id) + { + let event = FocusOutEvent { + blurred: WeakFocusHandle { + id: blurred_id, + handles: Arc::downgrade(&cx.focus_handles), + }, + }; + listener(event, window, cx) + } + true + })); + cx.defer(move |_| activate()); + subscription + } + + fn reset_cursor_style(&self, cx: &mut App) { + // Set the cursor only if we're the active window. + if self.is_window_hovered() { + let style = self + .rendered_frame + .cursor_style(self) + .unwrap_or(CursorStyle::Arrow); + cx.platform.set_cursor_style(style); + } + } + + /// Dispatch a given keystroke as though the user had typed it. + /// You can create a keystroke with Keystroke::parse(""). + pub fn dispatch_keystroke(&mut self, keystroke: Keystroke, cx: &mut App) -> bool { + let keystroke = keystroke.with_simulated_ime(); + let result = self.dispatch_event( + PlatformInput::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held: false, + prefer_character_input: false, + }), + cx, + ); + if !result.propagate { + return true; + } + + if let Some(input) = keystroke.key_char + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler); + return true; + } + + false + } + + /// Return a key binding string for an action, to display in the UI. Uses the highest precedence + /// binding for the action (last binding added to the keymap). + pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + self.highest_precedence_binding_for_action(action) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) + } + + /// Dispatch a mouse, keyboard, or touch event on the window. + #[profiling::function] + pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { + #[cfg(feature = "profiler")] + self.window_profiler.begin_input(event.kind_name()); + let update_count_before = self.invalidator.update_count(); + // Track input modality for focus-visible styling and hover suppression. + // Hover is suppressed during keyboard modality so that keyboard navigation + // doesn't show hover highlights on the item under the mouse cursor. + let old_modality = self.last_input_modality; + self.last_input_modality = match &event { + PlatformInput::KeyDown(_) => InputModality::Keyboard, + PlatformInput::MouseMove(_) | PlatformInput::MouseDown(_) => InputModality::Mouse, + PlatformInput::Touch(_) => InputModality::Touch, + _ => self.last_input_modality, + }; + if self.last_input_modality != old_modality { + self.refresh(); + } + + // Handlers may set this to false by calling `stop_propagation`. + cx.propagate_event = true; + // Handlers may set this to true by calling `prevent_default`. + self.default_prevented = false; + + let event = match event { + // Track the mouse position with our own state, since accessing the platform + // API for the mouse position can only occur on the main thread. + PlatformInput::MouseMove(mouse_move) => { + self.mouse_position = mouse_move.position; + self.modifiers = mouse_move.modifiers; + PlatformInput::MouseMove(mouse_move) + } + PlatformInput::MouseDown(mouse_down) => { + self.mouse_position = mouse_down.position; + self.modifiers = mouse_down.modifiers; + PlatformInput::MouseDown(mouse_down) + } + PlatformInput::MouseUp(mouse_up) => { + self.mouse_position = mouse_up.position; + self.modifiers = mouse_up.modifiers; + PlatformInput::MouseUp(mouse_up) + } + PlatformInput::MousePressure(mouse_pressure) => { + PlatformInput::MousePressure(mouse_pressure) + } + PlatformInput::MouseExited(mouse_exited) => { + self.modifiers = mouse_exited.modifiers; + PlatformInput::MouseExited(mouse_exited) + } + PlatformInput::ModifiersChanged(modifiers_changed) => { + self.modifiers = modifiers_changed.modifiers; + self.capslock = modifiers_changed.capslock; + PlatformInput::ModifiersChanged(modifiers_changed) + } + PlatformInput::ScrollWheel(scroll_wheel) => { + self.mouse_position = scroll_wheel.position; + self.modifiers = scroll_wheel.modifiers; + PlatformInput::ScrollWheel(scroll_wheel) + } + PlatformInput::Pinch(pinch) => { + self.mouse_position = pinch.position; + self.modifiers = pinch.modifiers; + PlatformInput::Pinch(pinch) + } + // Translate dragging and dropping of external files from the operating system + // to internal drag and drop events. + PlatformInput::FileDrop(file_drop) => match file_drop { + FileDropEvent::Entered { position, paths } => { + self.mouse_position = position; + let source_window = self.handle.window_id(); + if !cx.restore_platform_drag(source_window) && cx.active_drag.is_none() { + cx.active_drag = Some(AnyDrag { + value: Arc::new(paths.clone()), + view: cx.new(|_| paths).into(), + cursor_offset: position, + cursor_style: None, + external_payload_source: None, + }); + } + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Pending { position } => { + self.mouse_position = position; + PlatformInput::MouseMove(MouseMoveEvent { + position, + pressed_button: Some(MouseButton::Left), + modifiers: Modifiers::default(), + }) + } + FileDropEvent::Submit { position } => { + cx.activate(true); + self.mouse_position = position; + PlatformInput::MouseUp(MouseUpEvent { + button: MouseButton::Left, + position, + modifiers: Modifiers::default(), + click_count: 1, + }) + } + FileDropEvent::Exited => { + if !cx.hand_restored_drag_to_platform(self.handle.window_id()) { + cx.active_drag.take(); + } + self.refresh(); + PlatformInput::FileDrop(FileDropEvent::Exited) + } + FileDropEvent::Ended => { + cx.end_platform_drag(self.handle.window_id()); + self.refresh(); + PlatformInput::FileDrop(FileDropEvent::Ended) + } + }, + PlatformInput::Touch(touch) => PlatformInput::Touch(touch), + PlatformInput::LongPress(long_press) => { + self.mouse_position = if long_press.phase == crate::TouchPhase::Started { + long_press.start_position + } else { + long_press.position + }; + if long_press.phase == crate::TouchPhase::Started { + self.long_press_capture = None; + } + PlatformInput::LongPress(long_press) + } + PlatformInput::TouchDrag(touch_drag) => { + self.mouse_position = touch_drag.start_position; + PlatformInput::TouchDrag(touch_drag) + } + PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, + }; + + if let Some(any_mouse_event) = event.mouse_event() { + self.dispatch_mouse_event(any_mouse_event, cx); + } else if let Some(any_key_event) = event.keyboard_event() { + self.dispatch_key_event(any_key_event, cx); + } else if let Some(touch_event) = event.touch_event() { + self.dispatch_touch_event(touch_event, cx); + } + if let PlatformInput::LongPress(long_press) = &event { + match long_press.phase { + crate::TouchPhase::Started if !self.default_prevented => { + self.long_press_capture = None; + } + crate::TouchPhase::Ended | crate::TouchPhase::Cancelled => { + self.long_press_capture = None; + } + crate::TouchPhase::Started | crate::TouchPhase::Moved => {} + } + } + + // Must run after the move is dispatched: the platform owns the gesture afterwards, so this + // is the last chance for drag listeners to see the pointer leave and reset their state. + self.promote_external_drag_to_platform(&event, cx); + + let caused_invalidation = self.invalidator.update_count() > update_count_before; + if caused_invalidation { + self.input_rate_tracker.borrow_mut().record_input(); + } + #[cfg(feature = "profiler")] + self.window_profiler.end_input(caused_invalidation); + + DispatchEventResult { + propagate: cx.propagate_event, + default_prevented: self.default_prevented, + } + } + + fn promote_external_drag_to_platform(&mut self, event: &PlatformInput, cx: &mut App) { + let PlatformInput::MouseMove(mouse_move) = event else { + return; + }; + if mouse_move.pressed_button != Some(MouseButton::Left) { + return; + } + if Bounds::new(Point::default(), self.viewport_size).contains(&mouse_move.position) { + return; + } + if !self.platform_window.can_start_external_drag() { + return; + } + let Some(payload_source) = cx + .active_drag + .as_mut() + .and_then(|drag| drag.external_payload_source.take()) + else { + return; + }; + let Some(payload) = payload_source(self, cx) else { + return; + }; + if self.platform_window.start_external_drag(&payload) + && cx.hand_active_drag_to_platform(self.handle.window_id()) + { + self.refresh(); + } + } + + /// Whether recognized touch pans may use the platform's predicted touch + /// positions ([`TouchEvent::predicted_position`]) to compensate for input + /// latency. Defaults to true. + pub fn touch_prediction_enabled(&self) -> bool { + self.touch_prediction_enabled + } + + /// Sets whether recognized touch pans may use the platform's predicted + /// touch positions. Disabling drops [`TouchEvent::predicted_position`] + /// before gesture recognition, so pans track only raw touch positions. + pub fn set_touch_prediction_enabled(&mut self, enabled: bool) { + self.touch_prediction_enabled = enabled; + } + + /// Runs the portable gesture recognizer over a raw touch event and + /// dispatches whatever it resolves (scroll steps, synthesized taps) + /// through the ordinary mouse-event path. + fn dispatch_touch_event(&mut self, event: &TouchEvent, cx: &mut App) { + let mut event = event.clone(); + if !self.touch_prediction_enabled { + event.predicted_position = None; + } + let recognized_gestures = self.touch_gestures.handle_event(&event); + if event.phase == crate::TouchPhase::Started + && let Some(touch_drag) = self.touch_gestures.offer_touch_drag(event.id) + { + self.dispatch_recognized_touch_gesture(touch_drag, cx); + } + if event.phase == crate::TouchPhase::Started + && self.touch_gestures.pending_long_press().is_some() + { + self.long_press_capture = None; + } + let mut tapped = false; + for gesture in recognized_gestures { + tapped |= matches!(gesture, RecognizedTouchGesture::Tap { .. }); + self.dispatch_recognized_touch_gesture(gesture, cx); + } + if event.phase == crate::TouchPhase::Started { + self.schedule_long_press_timer(cx); + } else if self.touch_gestures.pending_long_press().is_none() { + self.long_press_timer.take(); + } + // The platform's touch-release handler may inspect the input handler + // as soon as this dispatch returns (the web platform decides virtual + // keyboard visibility there, inside the user gesture). Input handlers + // are registered during draw, so draw now to make them reflect any + // focus change the tap just caused. + if tapped && self.invalidator.is_dirty() { + self.draw(cx).clear(cx); + } + if self.touch_gestures.has_momentum() { + self.schedule_touch_momentum_tick(); + } + } + + fn dispatch_recognized_touch_gesture(&mut self, gesture: RecognizedTouchGesture, cx: &mut App) { + match gesture { + RecognizedTouchGesture::Scroll(scroll_wheel) => { + self.mouse_position = scroll_wheel.position; + cx.propagate_event = true; + self.dispatch_mouse_event(&scroll_wheel, cx); + } + RecognizedTouchGesture::Tap { down, up } => { + self.mouse_position = up.position; + cx.propagate_event = true; + self.dispatch_mouse_event(&down, cx); + cx.propagate_event = true; + self.dispatch_mouse_event(&up, cx); + } + RecognizedTouchGesture::TouchDrag(touch_drag) => { + self.mouse_position = touch_drag.start_position; + cx.propagate_event = true; + self.default_prevented = false; + let started = touch_drag.phase == crate::TouchPhase::Started; + self.dispatch_mouse_event(&touch_drag, cx); + if started { + self.touch_gestures + .resolve_touch_drag(self.default_prevented); + } + } + RecognizedTouchGesture::LongPress(long_press) => { + self.mouse_position = if long_press.phase == crate::TouchPhase::Started { + long_press.start_position + } else { + long_press.position + }; + cx.propagate_event = true; + self.default_prevented = false; + let started = long_press.phase == crate::TouchPhase::Started; + let ended = matches!( + long_press.phase, + crate::TouchPhase::Ended | crate::TouchPhase::Cancelled + ); + self.dispatch_mouse_event(&long_press, cx); + if started { + let claimed = self.default_prevented; + self.touch_gestures.resolve_long_press(claimed); + if !claimed { + self.long_press_capture = None; + } + } + if ended { + self.long_press_capture = None; + } + } + } + } + + fn schedule_long_press_timer(&mut self, cx: &mut App) { + self.long_press_timer.take(); + let Some((touch_id, duration)) = self.touch_gestures.pending_long_press() else { + return; + }; + self.long_press_timer = Some(self.spawn(cx, async move |cx| { + cx.background_executor.timer(duration).await; + cx.update(move |window, cx| { + window.long_press_timer.take(); + if let Some(gesture) = window.touch_gestures.offer_long_press(touch_id) { + window.dispatch_recognized_touch_gesture(gesture, cx); + } + }) + .log_err(); + })); + } + + fn schedule_touch_momentum_tick(&mut self) { + self.on_next_frame(|window, cx| { + if let Some(gesture) = window.touch_gestures.tick_momentum() { + window.dispatch_recognized_touch_gesture(gesture, cx); + } + if window.touch_gestures.has_momentum() { + window.schedule_touch_momentum_tick(); + } + }); + } + + fn dispatch_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { + let hit_test = self.rendered_frame.hit_test(self.mouse_position()); + if hit_test != self.mouse_hit_test { + self.mouse_hit_test = hit_test; + self.reset_cursor_style(cx); + } + + #[cfg(any(feature = "inspector", debug_assertions))] + if self.is_inspector_picking(cx) { + self.handle_inspector_mouse_event(event, cx); + // When inspector is picking, all other mouse handling is skipped. + return; + } + + let mut mouse_listeners = mem::take(&mut self.rendered_frame.mouse_listeners); + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for listener in &mut mouse_listeners { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Capture, self, cx); + if !cx.propagate_event { + break; + } + } + + // Bubble phase, where most normal handlers do their work. + if cx.propagate_event { + for listener in mouse_listeners.iter_mut().rev() { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Bubble, self, cx); + if !cx.propagate_event { + break; + } + } + } + + self.rendered_frame.mouse_listeners = mouse_listeners; + + if cx.has_active_drag() { + if event.is::() { + // If this was a mouse move event, redraw the window so that the + // active drag can follow the mouse cursor. + self.refresh(); + } else if event.is::() { + // If this was a mouse up event, cancel the active drag and redraw + // the window. + cx.active_drag = None; + self.refresh(); + } + } + + // Auto-release pointer capture on mouse up + if event.is::() && self.captured_hitbox.is_some() { + self.captured_hitbox = None; + } + } + + // If modifiers are not excessive (e.g. AltGr), and the input handler is accepting text input, + // we prefer the text input over bindings. + fn input_preference(&mut self, event: &dyn Any, cx: &mut App) -> InputPreference { + let prefer_character_input = event + .downcast_ref::() + .is_some_and(|key_down_event| key_down_event.prefer_character_input); + if !prefer_character_input { + return InputPreference::KeyBindings; + } + let Some(mut input_handler) = self.platform_window.take_input_handler() else { + return InputPreference::KeyBindings; + }; + let accepts_text_input = input_handler.accepts_text_input(self, cx); + self.platform_window.set_input_handler(input_handler); + if accepts_text_input { + InputPreference::CharacterInput + } else { + InputPreference::KeyBindings + } + } + + fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) { + if self.invalidator.is_dirty() { + self.draw(cx).clear(cx); + } + + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + let mut keystroke: Option = None; + + if let Some(event) = event.downcast_ref::() { + if event.modifiers.number_of_modifiers() == 0 + && self.pending_modifier.modifiers.number_of_modifiers() == 1 + && !self.pending_modifier.saw_other_input + { + let key = match self.pending_modifier.modifiers { + modifiers if modifiers.shift => Some("shift"), + modifiers if modifiers.control => Some("control"), + modifiers if modifiers.alt => Some("alt"), + modifiers if modifiers.platform => Some("platform"), + modifiers if modifiers.function => Some("function"), + _ => None, + }; + if let Some(key) = key { + keystroke = Some(Keystroke { + key: key.to_string(), + key_char: None, + modifiers: Modifiers::default(), + }); + } + } + + if self.pending_modifier.modifiers.number_of_modifiers() == 0 + && event.modifiers.number_of_modifiers() == 1 + { + self.pending_modifier.saw_other_input = false + } else if event.modifiers.number_of_modifiers() > 1 { + self.pending_modifier.saw_other_input = true + } + self.pending_modifier.modifiers = event.modifiers + } else if let Some(key_down_event) = event.downcast_ref::() { + self.pending_modifier.saw_other_input = true; + keystroke = Some(key_down_event.keystroke.clone()); + if key_down_event.keystroke.key_char.is_some() + && matches!( + cx.cursor_hide_mode, + CursorHideMode::OnTyping | CursorHideMode::OnTypingAndAction + ) + { + cx.platform.hide_cursor_until_mouse_moves(); + } + } + + let Some(keystroke) = keystroke else { + self.finish_dispatch_key_event( + event, + None, + InputPreference::KeyBindings, + dispatch_path, + self.context_stack(), + cx, + ); + return; + }; + + let input_preference = self.input_preference(event, cx); + + cx.propagate_event = true; + self.dispatch_keystroke_interceptors( + &keystroke, + input_preference, + self.context_stack(), + cx, + ); + if !cx.propagate_event { + self.finish_dispatch_key_event( + event, + Some(&keystroke), + input_preference, + dispatch_path, + self.context_stack(), + cx, + ); + return; + } + + let mut currently_pending = self.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.focus { + currently_pending = PendingInput::default(); + } + + let match_result = self.rendered_frame.dispatch_tree.dispatch_key( + currently_pending.keystrokes, + keystroke.clone(), + &dispatch_path, + ); + + if !match_result.to_replay.is_empty() { + self.replay_pending_input(match_result.to_replay, cx); + cx.propagate_event = true; + } + + if !match_result.pending.is_empty() { + let previous_timeout = currently_pending.timeout.take(); + currently_pending.keystrokes = match_result.pending; + currently_pending.focus = self.focus; + + let text_input_requires_timeout = event + .downcast_ref::() + .filter(|key_down| key_down.keystroke.key_char.is_some()) + .and_then(|_| self.platform_window.take_input_handler()) + .map_or(false, |mut input_handler| { + let accepts = input_handler.accepts_text_input(self, cx); + self.platform_window.set_input_handler(input_handler); + accepts + }); + + let needs_timeout = previous_timeout.is_some() + || match_result.pending_has_binding + || text_input_requires_timeout; + currently_pending.timeout = if needs_timeout { + match previous_timeout { + Some(mut timeout) if timeout.is_paused() => { + timeout.reset_duration(PENDING_INPUT_TIMEOUT); + Some(timeout) + } + previous_timeout => { + drop(previous_timeout); + Some(self.new_pending_input_timeout(PENDING_INPUT_TIMEOUT, cx)) + } + } + } else { + None + }; + self.pending_input = Some(currently_pending); + self.pending_input_changed(cx); + cx.propagate_event = false; + return; + } + + // Interceptors or replayed actions may have changed whether the input handler accepts text. + let input_preference = self.input_preference(event, cx); + if input_preference == InputPreference::KeyBindings { + for binding in match_result.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); + if !cx.propagate_event { + self.dispatch_keystroke_observers( + &keystroke, + input_preference, + Some(binding.action.as_ref()), + match_result.context_stack, + cx, + ); + self.pending_input_changed(cx); + return; + } + } + } + + self.finish_dispatch_key_event( + event, + Some(&keystroke), + input_preference, + dispatch_path, + match_result.context_stack, + cx, + ); + self.pending_input_changed(cx); + } + + fn new_pending_input_timeout(&self, duration: Duration, cx: &App) -> PendingInputTimeout { + let (started_at, task) = self.start_pending_input_timeout(duration, cx); + PendingInputTimeout { + duration, + remaining: duration, + state: PendingInputTimeoutState::Running { started_at, task }, + } + } + + fn start_pending_input_timeout(&self, remaining: Duration, cx: &App) -> (Instant, Task<()>) { + let started_at = cx.background_executor().now(); + let task = self.spawn(cx, async move |cx| { + cx.background_executor.timer(remaining).await; + cx.update(move |window, cx| { + let Some(currently_pending) = window + .pending_input + .take() + .filter(|pending| pending.focus == window.focus) + else { + return; + }; + + let node_id = window.focus_node_id_in_rendered_frame(window.focus); + let dispatch_path = window.rendered_frame.dispatch_tree.dispatch_path(node_id); + + let to_replay = window + .rendered_frame + .dispatch_tree + .flush_dispatch(currently_pending.keystrokes, &dispatch_path); + + window.pending_input_changed(cx); + window.replay_pending_input(to_replay, cx) + }) + .log_err(); + }); + (started_at, task) + } + + fn finish_dispatch_key_event( + &mut self, + event: &dyn Any, + recognized_keystroke: Option<&Keystroke>, + input_preference: InputPreference, + dispatch_path: SmallVec<[DispatchNodeId; 32]>, + context_stack: Vec, + cx: &mut App, + ) { + self.dispatch_key_down_up_event(event, &dispatch_path, cx); + if !cx.propagate_event { + return; + } + + self.dispatch_modifiers_changed_event(event, &dispatch_path, cx); + if !cx.propagate_event { + return; + } + + if let Some(keystroke) = recognized_keystroke { + self.dispatch_keystroke_observers(keystroke, input_preference, None, context_stack, cx); + } + } + + pub(crate) fn pending_input_changed(&mut self, cx: &mut App) { + self.pending_input_observers + .clone() + .retain(&(), |callback| callback(self, cx)); + } + + fn defer_pending_input_changed(&self, cx: &mut App) { + // Avoid re-entrant entity updates by deferring observer notifications to the end of the + // current effect cycle, and only for this window. + let window_handle = self.handle; + cx.defer(move |cx| { + window_handle + .update(cx, |_, window, cx| { + window.pending_input_changed(cx); + }) + .ok(); + }); + } + + fn dispatch_key_down_up_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + cx: &mut App, + ) { + // Capture phase + for node_id in dispatch_path { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self, cx); + if !cx.propagate_event { + return; + } + } + } + } + + fn dispatch_modifiers_changed_event( + &mut self, + event: &dyn Any, + dispatch_path: &SmallVec<[DispatchNodeId; 32]>, + cx: &mut App, + ) { + let Some(event) = event.downcast_ref::() else { + return; + }; + for node_id in dispatch_path.iter().rev() { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for listener in node.modifiers_changed_listeners.clone() { + listener(event, self, cx); + if !cx.propagate_event { + return; + } + } + } + } + + /// Determine whether a potential multi-stroke key binding is in progress on this window. + pub fn has_pending_keystrokes(&self) -> bool { + self.pending_input().is_some() + } + + #[cfg(test)] + pub(crate) fn pending_input_is_none(&self) -> bool { + self.pending_input.is_none() + } + + pub(crate) fn clear_pending_keystrokes(&mut self, cx: &mut App) { + if self.pending_input.take().is_some() { + self.defer_pending_input_changed(cx); + } + } + + /// Returns pending input that can still complete a multi-stroke key binding. Input left over + /// from a previous focus can never complete one. + pub fn pending_input(&self) -> Option> { + self.pending_input + .as_ref() + .filter(|pending_input| pending_input.focus == self.focus) + .map(|pending_input| PendingInputStatus { + keystrokes: pending_input.keystrokes.as_slice(), + timeout: pending_input + .timeout + .as_ref() + .map(PendingInputTimeout::status), + }) + } + + /// Pauses or resumes the current pending input timeout on behalf of `owner`. + /// + /// A paused timeout resumes automatically if `owner` is released. Returns whether the timeout + /// state changed. A timeout paused by one owner cannot be resumed by another. + pub fn set_pending_input_timeout_paused( + &mut self, + owner: &Entity, + paused: bool, + cx: &mut App, + ) -> bool { + let owner_id = owner.entity_id(); + if !paused { + return self.resume_pending_input_timeout(owner_id, cx); + } + + let timeout = self + .pending_input + .as_ref() + .filter(|pending_input| pending_input.focus == self.focus) + .and_then(|pending_input| pending_input.timeout.as_ref()); + let Some(timeout) = timeout else { + return false; + }; + if timeout.is_paused() { + return false; + } + + let release_subscription = self.observe_release(owner, cx, move |_, window, cx| { + window.resume_pending_input_timeout(owner_id, cx); + }); + let now = cx.background_executor().now(); + let changed = self + .pending_input + .as_mut() + .filter(|pending_input| pending_input.focus == self.focus) + .and_then(|pending_input| pending_input.timeout.as_mut()) + .is_some_and(|timeout| { + timeout.pause( + PendingInputTimeoutPause { + owner_id, + _release_subscription: release_subscription, + }, + now, + ) + }); + + if changed { + self.defer_pending_input_changed(cx); + } + changed + } + + fn resume_pending_input_timeout(&mut self, owner_id: EntityId, cx: &mut App) -> bool { + let Some(remaining) = self + .pending_input + .as_ref() + .and_then(|pending_input| pending_input.timeout.as_ref()) + .filter(|timeout| timeout.pause_owner_id() == Some(owner_id)) + .map(|timeout| timeout.remaining) + else { + return false; + }; + + let (started_at, task) = self.start_pending_input_timeout(remaining, cx); + let changed = self + .pending_input + .as_mut() + .and_then(|pending_input| pending_input.timeout.as_mut()) + .is_some_and(|timeout| timeout.resume(owner_id, started_at, task)); + + if changed { + self.defer_pending_input_changed(cx); + } + changed + } + + /// Returns the currently pending input keystrokes that might result in a multi-stroke key binding. + pub fn pending_input_keystrokes(&self) -> Option<&[Keystroke]> { + self.pending_input() + .map(|pending_input| pending_input.keystrokes()) + } + + fn replay_pending_input(&mut self, replays: SmallVec<[Replay; 1]>, cx: &mut App) { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + 'replay: for replay in replays { + let event = KeyDownEvent { + keystroke: replay.keystroke.clone(), + is_held: false, + prefer_character_input: true, + }; + + cx.propagate_event = true; + for binding in replay.bindings { + self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); + if !cx.propagate_event { + self.dispatch_keystroke_observers( + &replay.keystroke, + InputPreference::KeyBindings, + Some(binding.action.as_ref()), + Vec::default(), + cx, + ); + continue 'replay; + } + } + + self.dispatch_key_down_up_event(&event, &dispatch_path, cx); + if !cx.propagate_event { + continue 'replay; + } + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() + && let Some(mut input_handler) = self.platform_window.take_input_handler() + { + input_handler.dispatch_input(&input, self, cx); + self.platform_window.set_input_handler(input_handler) + } + } + } + + fn focus_node_id_in_rendered_frame(&self, focus_id: Option) -> DispatchNodeId { + focus_id + .and_then(|focus_id| { + self.rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.rendered_frame.dispatch_tree.root_node_id()) + } + + fn dispatch_action_on_node( + &mut self, + node_id: DispatchNodeId, + action: &dyn Action, + cx: &mut App, + ) { + self.dispatch_action_on_node_inner(node_id, action, cx); + + if !cx.propagate_event + && cx.cursor_hide_mode == CursorHideMode::OnTypingAndAction + && self.last_input_was_keyboard() + { + cx.platform.hide_cursor_until_mouse_moves(); + } + } + + fn dispatch_action_on_node_inner( + &mut self, + node_id: DispatchNodeId, + action: &dyn Action, + cx: &mut App, + ) { + let dispatch_path = self.rendered_frame.dispatch_tree.dispatch_path(node_id); + + // Capture phase for global actions. + cx.propagate_event = true; + if let Some(mut global_listeners) = cx + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + #[cfg(feature = "profiler")] + self.window_profiler.begin_action_handler(action, cx); + listener(action.as_any(), DispatchPhase::Capture, cx); + #[cfg(feature = "profiler")] + self.window_profiler.end_action_handler(); + if !cx.propagate_event { + break; + } + } + + global_listeners.extend( + cx.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + cx.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if !cx.propagate_event { + return; + } + + // Capture phase for window actions. + for node_id in &dispatch_path { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + #[cfg(feature = "profiler")] + self.window_profiler.begin_action_handler(action, cx); + listener(any_action, DispatchPhase::Capture, self, cx); + #[cfg(feature = "profiler")] + self.window_profiler.end_action_handler(); + + if !cx.propagate_event { + return; + } + } + } + } + + // Bubble phase for window actions. + for node_id in dispatch_path.iter().rev() { + let node = self.rendered_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + #[cfg(feature = "profiler")] + self.window_profiler.begin_action_handler(action, cx); + listener(any_action, DispatchPhase::Bubble, self, cx); + #[cfg(feature = "profiler")] + self.window_profiler.end_action_handler(); + + if !cx.propagate_event { + return; + } + } + } + } + + // Bubble phase for global actions. + if let Some(mut global_listeners) = cx + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + + #[cfg(feature = "profiler")] + self.window_profiler.begin_action_handler(action, cx); + listener(action.as_any(), DispatchPhase::Bubble, cx); + #[cfg(feature = "profiler")] + self.window_profiler.end_action_handler(); + if !cx.propagate_event { + break; + } + } + + global_listeners.extend( + cx.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + cx.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + + /// Register the given handler to be invoked whenever the global of the given type + /// is updated. + pub fn observe_global( + &mut self, + cx: &mut App, + f: impl Fn(&mut Window, &mut App) + 'static, + ) -> Subscription { + let window_handle = self.handle; + let (subscription, activate) = cx.global_observers.insert( + TypeId::of::(), + Box::new(move |cx| { + window_handle + .update(cx, |_, window, cx| f(window, cx)) + .is_ok() + }), + ); + cx.defer(move |_| activate()); + subscription + } + + /// Focus the current window and bring it to the foreground at the platform level. + pub fn activate_window(&self) { + self.platform_window.activate(); + } + + /// Requests that the operating system draw attention to this window. + pub fn request_attention(&self) { + self.platform_window.request_attention(); + } + + /// Minimize the current window at the platform level. + pub fn minimize_window(&self) { + self.platform_window.minimize(); + } + + /// Toggle full screen status on the current window at the platform level. + pub fn toggle_fullscreen(&self) { + self.platform_window.toggle_fullscreen(); + } + + /// Toggle simple (borderless) fullscreen, where the window covers the entire + /// screen including the menu bar and, on notched displays, the area around the + /// notch. Unlike [`Window::toggle_fullscreen`], this does not move the window + /// into its own Mission Control space. Only has an effect on macOS. + pub fn toggle_simple_fullscreen(&self) { + self.platform_window.toggle_simple_fullscreen(); + } + + /// Updates the IME panel position suggestions for languages like japanese, chinese. + pub fn invalidate_character_coordinates(&self) { + self.on_next_frame(|window, cx| { + if let Some(mut input_handler) = window.platform_window.take_input_handler() { + if let Some(bounds) = input_handler.selected_bounds(window, cx) { + window.platform_window.update_ime_position(bounds); + } + window.platform_window.set_input_handler(input_handler); + } + }); + } + + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. + pub fn prompt( + &mut self, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[T], + cx: &mut App, + ) -> oneshot::Receiver + where + T: Clone + Into, + { + let prompt_builder = cx.prompt_builder.take(); + let Some(prompt_builder) = prompt_builder else { + unreachable!("Re-entrant window prompting is not supported by GPUI"); + }; + + let answers = answers + .iter() + .map(|answer| answer.clone().into()) + .collect::>(); + + let receiver = match &prompt_builder { + PromptBuilder::Default => self + .platform_window + .prompt(level, message, detail, &answers) + .unwrap_or_else(|| { + self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) + }), + PromptBuilder::Custom(_) => { + self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx) + } + }; + + cx.prompt_builder = Some(prompt_builder); + + receiver + } + + fn build_custom_prompt( + &mut self, + prompt_builder: &PromptBuilder, + level: PromptLevel, + message: &str, + detail: Option<&str>, + answers: &[PromptButton], + cx: &mut App, + ) -> oneshot::Receiver { + let (sender, receiver) = oneshot::channel(); + let handle = PromptHandle::new(sender); + let handle = (prompt_builder)(level, message, detail, answers, handle, self, cx); + self.prompt = Some(handle); + receiver + } + + /// Returns whether a prompt rendered by GPUI is currently active in this window. + /// + /// This is only true for prompts rendered in the window (see + /// [`App::set_prompt_builder`]), not for platform-native prompt dialogs. + pub fn has_active_prompt(&self) -> bool { + self.prompt.is_some() + } + + /// Returns the current context stack. + pub fn context_stack(&self) -> Vec { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree + .dispatch_path(node_id) + .iter() + .filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone()) + .collect() + } + + /// Returns all available actions for the focused element. + pub fn available_actions(&self, cx: &App) -> Vec> { + let node_id = self.focus_node_id_in_rendered_frame(self.focus); + let mut actions = self.rendered_frame.dispatch_tree.available_actions(node_id); + for action_type in cx.global_action_listeners.keys() { + if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) { + let action = cx.actions.build_action_type(action_type).ok(); + if let Some(action) = action { + actions.insert(ix, action); + } + } + } + actions + } + + /// Returns key bindings that invoke an action on the currently focused element. Bindings are + /// returned in the order they were added. For display, the last binding should take precedence. + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.rendered_frame + .dispatch_tree + .bindings_for_action(action, &self.rendered_frame.dispatch_tree.context_stack) + } + + /// Returns the highest precedence key binding that invokes an action on the currently focused + /// element. This is more efficient than getting the last result of `bindings_for_action`. + pub fn highest_precedence_binding_for_action(&self, action: &dyn Action) -> Option { + self.rendered_frame + .dispatch_tree + .highest_precedence_binding_for_action( + action, + &self.rendered_frame.dispatch_tree.context_stack, + ) + } + + /// Returns the key bindings for an action in a context. + pub fn bindings_for_action_in_context( + &self, + action: &dyn Action, + context: KeyContext, + ) -> Vec { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree.bindings_for_action(action, &[context]) + } + + /// Returns the highest precedence key binding for an action in a context. This is more + /// efficient than getting the last result of `bindings_for_action_in_context`. + pub fn highest_precedence_binding_for_action_in_context( + &self, + action: &dyn Action, + context: KeyContext, + ) -> Option { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + dispatch_tree.highest_precedence_binding_for_action(action, &[context]) + } + + /// Returns any bindings that would invoke an action on the given focus handle if it were + /// focused. Bindings are returned in the order they were added. For display, the last binding + /// should take precedence. + pub fn bindings_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Vec { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let Some(context_stack) = self.context_stack_for_focus_handle(focus_handle) else { + return vec![]; + }; + dispatch_tree.bindings_for_action(action, &context_stack) + } + + /// Returns the highest precedence key binding that would invoke an action on the given focus + /// handle if it were focused. This is more efficient than getting the last result of + /// `bindings_for_action_in`. + pub fn highest_precedence_binding_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> Option { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let context_stack = self.context_stack_for_focus_handle(focus_handle)?; + dispatch_tree.highest_precedence_binding_for_action(action, &context_stack) + } + + /// Find the bindings that can follow the current input sequence for the current context stack. + pub fn possible_bindings_for_input(&self, input: &[Keystroke]) -> Vec { + self.rendered_frame + .dispatch_tree + .possible_next_bindings_for_input(input, &self.context_stack()) + } + + fn context_stack_for_focus_handle( + &self, + focus_handle: &FocusHandle, + ) -> Option> { + let dispatch_tree = &self.rendered_frame.dispatch_tree; + let node_id = dispatch_tree.focusable_node_id(focus_handle.id)?; + let context_stack: Vec<_> = dispatch_tree + .dispatch_path(node_id) + .into_iter() + .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) + .collect(); + Some(context_stack) + } + + /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. + pub fn listener_for( + &self, + view: &Entity, + f: impl Fn(&mut T, &E, &mut Window, &mut Context) + 'static, + ) -> impl Fn(&E, &mut Window, &mut App) + 'static { + let view = view.downgrade(); + move |e: &E, window: &mut Window, cx: &mut App| { + view.update(cx, |view, cx| f(view, e, window, cx)).ok(); + } + } + + /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. + pub fn handler_for) + 'static>( + &self, + entity: &Entity, + f: Callback, + ) -> impl Fn(&mut Window, &mut App) + 'static { + let entity = entity.downgrade(); + move |window: &mut Window, cx: &mut App| { + entity.update(cx, |entity, cx| f(entity, window, cx)).ok(); + } + } + + /// Register a callback that can interrupt the closing of the current window based the returned boolean. + /// If the callback returns false, the window won't be closed. + pub fn on_window_should_close( + &self, + cx: &App, + f: impl Fn(&mut Window, &mut App) -> bool + 'static, + ) { + let mut cx = self.to_async(cx); + self.platform_window.on_should_close(Box::new(move || { + cx.update(|window, cx| f(window, cx)).unwrap_or(true) + })) + } + + /// Register an action listener on this node for the next frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a listener yourself. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_action( + &mut self, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + self.next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } + + /// Register a capturing action listener on this node for the next frame if the condition is true. + /// The type of action is determined by the first parameter of the given listener. When the next + /// frame is rendered the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a listener yourself. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_action_when( + &mut self, + condition: bool, + action_type: TypeId, + listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, + ) { + self.invalidator.debug_assert_paint(); + + if condition { + self.next_frame + .dispatch_tree + .on_action(action_type, Rc::new(listener)); + } + } + + /// Read information about the GPU backing this window. + /// Currently returns None on Mac and Windows. + pub fn gpu_specs(&self) -> Option { + self.platform_window.gpu_specs() + } + + /// Perform titlebar double-click action. + /// This is macOS specific. + pub fn titlebar_double_click(&self) { + self.platform_window + .titlebar_double_click(self.is_resizable, self.is_minimizable); + } + + /// Gets the window's title at the platform level. + /// This is macOS specific. + pub fn window_title(&self) -> String { + self.platform_window.get_title() + } + + /// Returns a list of all tabbed windows and their titles. + /// This is macOS specific. + pub fn tabbed_windows(&self) -> Option> { + self.platform_window.tabbed_windows() + } + + /// Returns the tab bar visibility. + /// This is macOS specific. + pub fn tab_bar_visible(&self) -> bool { + self.platform_window.tab_bar_visible() + } + + /// Merges all open windows into a single tabbed window. + /// This is macOS specific. + pub fn merge_all_windows(&self) { + self.platform_window.merge_all_windows() + } + + /// Moves the tab to a new containing window. + /// This is macOS specific. + pub fn move_tab_to_new_window(&self) { + self.platform_window.move_tab_to_new_window() + } + + /// Shows or hides the window tab overview. + /// This is macOS specific. + pub fn toggle_window_tab_overview(&self) { + self.platform_window.toggle_window_tab_overview() + } + + /// Sets the tabbing identifier for the window. + /// This is macOS specific. + pub fn set_tabbing_identifier(&self, tabbing_identifier: Option) { + self.platform_window + .set_tabbing_identifier(tabbing_identifier) + } + + /// Request the OS to play an alert sound. On some platforms this is associated + /// with the window, for others it's just a simple global function call. + pub fn play_system_bell(&self) { + self.platform_window.play_system_bell() + } + + /// Returns whether accessibility support is enabled for this window. + /// + /// This is false when the app was created with [`crate::Application::new_inaccessible`]. + /// Unlike [`Self::is_a11y_active`], this does not depend on whether assistive + /// technology is currently connected, so it can be used to gate subscriptions + /// that are only needed for accessibility. + pub fn is_a11y_enabled(&self) -> bool { + self.a11y.is_enabled() + } + + /// Returns whether accessibility features are active for this frame, + /// i.e. whether assistive technology (such as a screen reader) is + /// connected and an accessibility tree is being built. + /// + /// Use this to skip computing data during rendering that is only + /// observable through the accessibility tree. When accessibility is + /// activated, a redraw is forced, so gated work is recomputed before the + /// next tree update is sent to the platform. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + pub fn is_a11y_active(&self) -> bool { + self.a11y.is_active() + } + + /// Debug representation of the last frame's accessibility information. + pub fn debug_a11y_tree_json(&self) -> Option { + self.a11y.debug_tree_json() + } + + /// Register a listener for an accessibility action on a specific node. + /// The listener will be called when a screen reader requests the given + /// action on the node identified by `node_id`. + /// + /// See the [accessibility guide](crate::_accessibility) for an overview. + pub fn on_a11y_action( + &mut self, + node_id: accesskit::NodeId, + action: accesskit::Action, + listener: impl FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static, + ) { + self.a11y + .action_listeners + .entry(node_id) + .or_default() + .push((action, Box::new(listener))); + } + + #[cfg(not(target_family = "wasm"))] + pub(crate) fn handle_a11y_action(&mut self, request: accesskit::ActionRequest, cx: &mut App) { + // Take listeners out temporarily so the closures can borrow Window + // mutably, then restore them afterward. + if let Some(mut listeners) = self.a11y.action_listeners.remove(&request.target_node) { + let extra_data = request.data.as_ref(); + let mut matched = false; + for (action, listener) in &mut listeners { + if *action == request.action { + listener(extra_data, self, cx); + matched = true; + } + } + self.a11y + .action_listeners + .insert(request.target_node, listeners); + if matched { + return; + } + } + + // Fall back to built-in action handling. + match request.action { + accesskit::Action::Click => { + if let Some(bounds) = self.a11y.node_bounds.get(&request.target_node).copied() { + let center = bounds.center(); + let mouse_down = PlatformInput::MouseDown(crate::MouseDownEvent { + button: MouseButton::Left, + position: center, + modifiers: Modifiers::default(), + click_count: 1, + first_mouse: false, + }); + let mouse_up = PlatformInput::MouseUp(MouseUpEvent { + button: MouseButton::Left, + position: center, + modifiers: Modifiers::default(), + click_count: 1, + }); + self.dispatch_event(mouse_down, cx); + self.dispatch_event(mouse_up, cx); + } + } + accesskit::Action::Focus => { + if let Some(focus_id) = self.a11y.focus_ids.get(&request.target_node).copied() + && let Some(handle) = FocusHandle::for_id(focus_id, &cx.focus_handles) + { + self.focus(&handle, cx); + } + } + accesskit::Action::Blur => { + self.blur(cx); + } + _ => { + log::debug!( + "Unhandled a11y action: {:?} on {:?}", + request.action, + request.target_node + ); + } + } + } + + /// Toggles the inspector mode on this window. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn toggle_inspector(&mut self, cx: &mut App) { + self.inspector = match self.inspector { + None => Some(cx.new(|_| Inspector::new())), + Some(_) => { + self.rendered_frame.next_inspector_instance_ids = FxHashMap::default(); + self.rendered_frame.inspector_hitboxes = FxHashMap::default(); + self.next_frame.next_inspector_instance_ids = FxHashMap::default(); + self.next_frame.inspector_hitboxes = FxHashMap::default(); + None + } + }; + self.refresh(); + } + + /// Returns true if the window is in inspector mode. + pub fn is_inspector_picking(&self, _cx: &App) -> bool { + #[cfg(any(feature = "inspector", debug_assertions))] + { + if let Some(inspector) = &self.inspector { + return inspector.read(_cx).is_picking(); + } + } + false + } + + /// Executes the provided function with mutable access to an inspector state. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn with_inspector_state( + &mut self, + inspector_id: Option<&crate::InspectorElementId>, + cx: &mut App, + f: impl FnOnce(&mut Option, &mut Self) -> R, + ) -> Option { + let inspector_id = inspector_id?; + let inspector = self.inspector.as_ref()?; + if inspector.read(cx).active_element_id() != Some(inspector_id) { + return None; + } + let inspector = inspector.clone(); + Some(inspector.update(cx, |inspector, _cx| { + inspector.with_active_element_state(self, f) + })) + } + + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) fn inspector_enabled(&self) -> bool { + self.inspector.is_some() + } + + #[cfg(any(feature = "inspector", debug_assertions))] + pub(crate) fn build_inspector_element_id( + &mut self, + path: crate::InspectorElementPath, + ) -> crate::InspectorElementId { + self.invalidator.debug_assert_paint_or_prepaint(); + let path = Rc::new(path); + let next_instance_id = self + .next_frame + .next_inspector_instance_ids + .entry(path.clone()) + .or_insert(0); + let instance_id = *next_instance_id; + *next_instance_id += 1; + crate::InspectorElementId { path, instance_id } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn prepaint_inspector(&mut self, inspector_width: Pixels, cx: &mut App) -> Option { + if let Some(inspector) = self.inspector.take() { + let mut inspector_element = AnyView::from(inspector.clone()).into_any_element(); + inspector_element.prepaint_as_root( + point(self.viewport_size.width - inspector_width, px(0.0)), + size(inspector_width, self.viewport_size.height).into(), + self, + cx, + ); + self.inspector = Some(inspector); + Some(inspector_element) + } else { + None + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn paint_inspector(&mut self, mut inspector_element: Option, cx: &mut App) { + if let Some(mut inspector_element) = inspector_element { + inspector_element.paint(self, cx); + }; + } + + /// Registers a hitbox that can be used for inspector picking mode, allowing users to select and + /// inspect UI elements by clicking on them. + #[cfg(any(feature = "inspector", debug_assertions))] + pub fn insert_inspector_hitbox( + &mut self, + hitbox_id: HitboxId, + inspector_id: Option<&crate::InspectorElementId>, + cx: &App, + ) { + self.invalidator.debug_assert_paint_or_prepaint(); + if !self.is_inspector_picking(cx) { + return; + } + if let Some(inspector_id) = inspector_id { + self.next_frame + .inspector_hitboxes + .insert(hitbox_id, inspector_id.clone()); + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn paint_inspector_hitbox(&mut self, cx: &App) { + if let Some(inspector) = self.inspector.as_ref() { + let inspector = inspector.read(cx); + if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame) + && let Some(hitbox) = self + .next_frame + .hitboxes + .iter() + .find(|hitbox| hitbox.id == hitbox_id) + { + self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d))); + } + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn handle_inspector_mouse_event(&mut self, event: &dyn Any, cx: &mut App) { + let Some(inspector) = self.inspector.clone() else { + return; + }; + if event.downcast_ref::().is_some() { + inspector.update(cx, |inspector, _cx| { + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.hover(inspector_id, self); + } + }); + } else if event.downcast_ref::().is_some() { + inspector.update(cx, |inspector, _cx| { + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.select(inspector_id, self); + } + }); + } else if let Some(event) = event.downcast_ref::() { + // This should be kept in sync with SCROLL_LINES in x11 platform. + const SCROLL_LINES: f32 = 3.0; + const SCROLL_PIXELS_PER_LAYER: f32 = 36.0; + let delta_y = event + .delta + .pixel_delta(px(SCROLL_PIXELS_PER_LAYER / SCROLL_LINES)) + .y; + if let Some(inspector) = self.inspector.clone() { + inspector.update(cx, |inspector, _cx| { + if let Some(depth) = inspector.pick_depth.as_mut() { + *depth += f32::from(delta_y) / SCROLL_PIXELS_PER_LAYER; + let max_depth = self.mouse_hit_test.ids.len() as f32 - 0.5; + if *depth < 0.0 { + *depth = 0.0; + } else if *depth > max_depth { + *depth = max_depth; + } + if let Some((_, inspector_id)) = + self.hovered_inspector_hitbox(inspector, &self.rendered_frame) + { + inspector.set_active_element_id(inspector_id, self); + } + } + }); + } + } + } + + #[cfg(any(feature = "inspector", debug_assertions))] + fn hovered_inspector_hitbox( + &self, + inspector: &Inspector, + frame: &Frame, + ) -> Option<(HitboxId, crate::InspectorElementId)> { + if let Some(pick_depth) = inspector.pick_depth { + let depth = (pick_depth as i64).try_into().unwrap_or(0); + let max_skipped = self.mouse_hit_test.ids.len().saturating_sub(1); + let skip_count = (depth as usize).min(max_skipped); + for hitbox_id in self.mouse_hit_test.ids.iter().skip(skip_count) { + if let Some(inspector_id) = frame.inspector_hitboxes.get(hitbox_id) { + return Some((*hitbox_id, inspector_id.clone())); + } + } + } + None + } + + /// For testing: set the current modifier keys state. + /// This does not generate any events. + #[cfg(any(test, feature = "test-support"))] + pub fn set_modifiers(&mut self, modifiers: Modifiers) { + self.modifiers = modifiers; + } + + /// For testing: simulate a mouse move event to the given position. + /// This dispatches the event through the normal event handling path, + /// which will trigger hover states and tooltips. + #[cfg(any(test, feature = "test-support"))] + pub fn simulate_mouse_move(&mut self, position: Point, cx: &mut App) { + let event = PlatformInput::MouseMove(MouseMoveEvent { + position, + modifiers: self.modifiers, + pressed_button: None, + }); + let _ = self.dispatch_event(event, cx); + } + + #[inline(never)] + fn take_element_state( + &mut self, + global_id: &GlobalElementId, + state_type: TypeId, + ) -> ((GlobalElementId, TypeId), Option) { + let key = (global_id.clone(), state_type); + self.next_frame.accessed_element_states.push(key.clone()); + let state = self + .next_frame + .element_states + .remove(&key) + .or_else(|| self.rendered_frame.element_states.remove(&key)); + (key, state) + } + + #[inline(never)] + fn insert_element_state( + &mut self, + key: (GlobalElementId, TypeId), + state: ElementStateBox, + ) -> Option { + self.next_frame.element_states.insert(key, state) + } + + #[inline(never)] + fn observe_keyed_state(state: &Entity, current_view: EntityId, cx: &mut App) { + cx.observe(state, move |_, cx| { + cx.notify(current_view); + }) + .detach(); + } +} + +// #[derive(Clone, Copy, Eq, PartialEq, Hash)] +slotmap::new_key_type! { + /// A unique identifier for a window. + pub struct WindowId; +} + +impl WindowId { + /// Converts this window ID to a `u64`. + pub fn as_u64(&self) -> u64 { + self.0.as_ffi() + } +} + +impl From for WindowId { + fn from(value: u64) -> Self { + WindowId(slotmap::KeyData::from_ffi(value)) + } +} + +/// A handle to a window with a specific root view type. +/// Note that this does not keep the window alive on its own. +#[derive(Deref, DerefMut)] +pub struct WindowHandle { + #[deref] + #[deref_mut] + pub(crate) any_handle: AnyWindowHandle, + state_type: PhantomData V>, +} + +impl Debug for WindowHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WindowHandle") + .field("any_handle", &self.any_handle.id.as_u64()) + .finish() + } +} + +impl WindowHandle { + /// Creates a new handle from a window ID. + /// This does not check if the root type of the window is `V`. + pub fn new(id: WindowId) -> Self { + WindowHandle { + any_handle: AnyWindowHandle { + id, + state_type: TypeId::of::(), + root_entity_type_name: std::any::type_name::(), + }, + state_type: PhantomData, + } + } + + /// Get the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + #[cfg(any(test, feature = "test-support"))] + pub fn root(&self, cx: &mut C) -> Result> + where + C: AppContext, + { + cx.update_window(self.any_handle, |root_view, _, _| { + root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed")) + })? + } + + /// Updates the root view of this window. + /// + /// This will fail if the window has been closed or if the root view's type does not match + pub fn update( + &self, + cx: &mut C, + update: impl FnOnce(&mut V, &mut Window, &mut Context) -> R, + ) -> Result + where + C: AppContext, + { + cx.update_window(self.any_handle, |root_view, window, cx| { + let view = root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(view.update(cx, |view, cx| update(view, window, cx))) + })? + } + + /// Read the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read<'a>(&self, cx: &'a App) -> Result<&'a V> { + let x = cx + .windows + .get(self.id) + .and_then(|window| { + window + .as_deref() + .and_then(|window| window.root.clone()) + .map(|root_view| root_view.downcast::()) + }) + .context("window not found")? + .map_err(|_| anyhow!("the type of the window's root view has changed"))?; + + Ok(x.read(cx)) + } + + /// Read the root view out of this window, with a callback + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &App) -> R) -> Result + where + C: AppContext, + { + cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) + } + + /// Read the root view pointer off of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. + pub fn entity(&self, cx: &C) -> Result> + where + C: AppContext, + { + cx.read_window(self, |root_view, _cx| root_view) + } + + /// Check if this window is 'active'. + /// + /// Will return `None` if the window is closed or currently + /// borrowed. + pub fn is_active(&self, cx: &mut App) -> Option { + cx.update_window(self.any_handle, |_, window, _| window.is_window_active()) + .ok() + } +} + +impl Copy for WindowHandle {} + +impl Clone for WindowHandle { + fn clone(&self) -> Self { + *self + } +} + +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.any_handle == other.any_handle + } +} + +impl Eq for WindowHandle {} + +impl Hash for WindowHandle { + fn hash(&self, state: &mut H) { + self.any_handle.hash(state); + } +} + +impl From> for AnyWindowHandle { + fn from(val: WindowHandle) -> Self { + val.any_handle + } +} + +/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct AnyWindowHandle { + pub(crate) id: WindowId, + state_type: TypeId, + root_entity_type_name: &'static str, +} + +impl AnyWindowHandle { + /// Get the ID of this window. + pub fn window_id(&self) -> WindowId { + self.id + } + + /// Returns the name of the window's declared root entity type. + pub fn root_entity_type_name(&self) -> &'static str { + self.root_entity_type_name + } + + /// Attempt to convert this handle to a window handle with a specific root view type. + /// If the types do not match, this will return `None`. + pub fn downcast(&self) -> Option> { + if TypeId::of::() == self.state_type { + Some(WindowHandle { + any_handle: *self, + state_type: PhantomData, + }) + } else { + None + } + } + + /// Updates the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn update( + self, + cx: &mut C, + update: impl FnOnce(AnyView, &mut Window, &mut App) -> R, + ) -> Result + where + C: AppContext, + { + cx.update_window(self, update) + } + + /// Read the state of the root view of this window. + /// + /// This will fail if the window has been closed. + pub fn read(self, cx: &C, read: impl FnOnce(Entity, &App) -> R) -> Result + where + C: AppContext, + T: 'static, + { + let view = self + .downcast::() + .context("the type of the window's root view has changed")?; + + cx.read_window(&view, read) + } +} + +impl HasWindowHandle for Window { + fn window_handle(&self) -> Result, HandleError> { + self.platform_window.window_handle() + } +} + +impl HasDisplayHandle for Window { + fn display_handle( + &self, + ) -> std::result::Result, HandleError> { + self.platform_window.display_handle() + } +} + +/// An identifier for an [`Element`]. +/// +/// Can be constructed with a string, a number, or both, as well +/// as other internal representations. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum ElementId { + /// The ID of a View element + View(EntityId), + /// An integer ID. + Integer(u64), + /// A string based ID. + Name(SharedString), + /// A UUID. + Uuid(Uuid), + /// An ID that's equated with a focus handle. + FocusHandle(FocusId), + /// A combination of a name and an integer. + NamedInteger(SharedString, u64), + /// A path. + Path(Arc), + /// A code location. + CodeLocation(core::panic::Location<'static>), + /// A labeled child of an element. + NamedChild(Arc, SharedString), + /// A byte array ID (used for text-anchors) + OpaqueId([u8; 20]), +} + +impl ElementId { + /// Constructs an `ElementId::NamedInteger` from a name and `usize`. + pub fn named_usize(name: impl Into, integer: usize) -> ElementId { + Self::NamedInteger(name.into(), integer as u64) + } +} + +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, + ElementId::Integer(ix) => write!(f, "{}", ix)?, + ElementId::Name(name) => write!(f, "{}", name)?, + ElementId::FocusHandle(_) => write!(f, "FocusHandle")?, + ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, + ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, + ElementId::Path(path) => write!(f, "{}", path.display())?, + ElementId::CodeLocation(location) => write!(f, "{}", location)?, + ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, + ElementId::OpaqueId(opaque_id) => write!(f, "{:x?}", opaque_id)?, + } + + Ok(()) + } +} + +impl TryInto for ElementId { + type Error = anyhow::Error; + + fn try_into(self) -> anyhow::Result { + if let ElementId::Name(name) = self { + Ok(name) + } else { + anyhow::bail!("element id is not string") + } + } +} + +impl From for ElementId { + fn from(id: u64) -> Self { + ElementId::Integer(id) + } +} + +impl From for ElementId { + fn from(id: usize) -> Self { + ElementId::Integer(id as u64) + } +} + +impl From for ElementId { + fn from(id: i32) -> Self { + Self::Integer(id as u64) + } +} + +impl From for ElementId { + fn from(name: SharedString) -> Self { + ElementId::Name(name) + } +} + +impl From for ElementId { + fn from(name: String) -> Self { + ElementId::Name(name.into()) + } +} + +impl From> for ElementId { + fn from(name: Arc) -> Self { + ElementId::Name(name.into()) + } +} + +impl From> for ElementId { + fn from(path: Arc) -> Self { + ElementId::Path(path) + } +} + +impl From<&'static str> for ElementId { + fn from(name: &'static str) -> Self { + ElementId::Name(SharedString::new_static(name)) + } +} + +impl<'a> From<&'a FocusHandle> for ElementId { + fn from(handle: &'a FocusHandle) -> Self { + ElementId::FocusHandle(handle.id) + } +} + +impl From<(&'static str, EntityId)> for ElementId { + fn from((name, id): (&'static str, EntityId)) -> Self { + ElementId::NamedInteger(SharedString::new_static(name), id.as_u64()) + } +} + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(SharedString::new_static(name), id as u64) + } +} + +impl From<(SharedString, usize)> for ElementId { + fn from((name, id): (SharedString, usize)) -> Self { + ElementId::NamedInteger(name, id as u64) + } +} + +impl From<(&'static str, u64)> for ElementId { + fn from((name, id): (&'static str, u64)) -> Self { + ElementId::NamedInteger(SharedString::new_static(name), id) + } +} + +impl From for ElementId { + fn from(value: Uuid) -> Self { + Self::Uuid(value) + } +} + +impl From<(&'static str, u32)> for ElementId { + fn from((name, id): (&'static str, u32)) -> Self { + ElementId::NamedInteger(SharedString::new_static(name), u64::from(id)) + } +} + +impl> From<(ElementId, T)> for ElementId { + fn from((id, name): (ElementId, T)) -> Self { + ElementId::NamedChild(Arc::new(id), name.into()) + } +} + +impl From<&'static core::panic::Location<'static>> for ElementId { + fn from(location: &'static core::panic::Location<'static>) -> Self { + ElementId::CodeLocation(*location) + } +} + +impl From<[u8; 20]> for ElementId { + fn from(opaque_id: [u8; 20]) -> Self { + ElementId::OpaqueId(opaque_id) + } +} + +/// A rectangle to be rendered in the window at the given position and size. +/// Passed as an argument [`Window::paint_quad`]. +#[derive(Clone)] +pub struct PaintQuad { + /// The bounds of the quad within the window. + pub bounds: Bounds, + /// The radii of the quad's corners. + pub corner_radii: Corners, + /// The background color of the quad. + pub background: Background, + /// The widths of the quad's borders. + pub border_widths: Edges, + /// The color of the quad's borders. + pub border_color: Hsla, + /// The style of the quad's borders. + pub border_style: BorderStyle, +} + +impl PaintQuad { + /// Sets the corner radii of the quad. + pub fn corner_radii(self, corner_radii: impl Into>) -> Self { + PaintQuad { + corner_radii: corner_radii.into(), + ..self + } + } + + /// Sets the border widths of the quad. + pub fn border_widths(self, border_widths: impl Into>) -> Self { + PaintQuad { + border_widths: border_widths.into(), + ..self + } + } + + /// Sets the border color of the quad. + pub fn border_color(self, border_color: impl Into) -> Self { + PaintQuad { + border_color: border_color.into(), + ..self + } + } + + /// Sets the background color of the quad. + pub fn background(self, background: impl Into) -> Self { + PaintQuad { + background: background.into(), + ..self + } + } +} + +/// Creates a quad with the given parameters. +pub fn quad( + bounds: Bounds, + corner_radii: impl Into>, + background: impl Into, + border_widths: impl Into>, + border_color: impl Into, + border_style: BorderStyle, +) -> PaintQuad { + PaintQuad { + bounds, + corner_radii: corner_radii.into(), + background: background.into(), + border_widths: border_widths.into(), + border_color: border_color.into(), + border_style, + } +} + +/// Creates a filled quad with the given bounds and background color. +pub fn fill(bounds: impl Into>, background: impl Into) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: background.into(), + border_widths: (0.).into(), + border_color: transparent_black(), + border_style: BorderStyle::default(), + } +} + +/// Creates a rectangle outline with the given bounds, border color, and a 1px border width +pub fn outline( + bounds: impl Into>, + border_color: impl Into, + border_style: BorderStyle, +) -> PaintQuad { + PaintQuad { + bounds: bounds.into(), + corner_radii: (0.).into(), + background: transparent_black().into(), + border_widths: (1.).into(), + border_color: border_color.into(), + border_style, + } +} + +#[inline(never)] +fn with_element_arena_erased(callback: &mut dyn FnMut(&mut Arena)) -> Result<(), AccessError> { + if let Some(arena_pointer) = CURRENT_ELEMENT_ARENA.try_with(Cell::get)? { + // SAFETY: The pointer is valid for the duration of the draw operation + // that set it, and we're being called during that same draw. + let arena_cell = unsafe { &*arena_pointer }; + callback(&mut arena_cell.borrow_mut()); + Ok(()) + } else { + ELEMENT_ARENA.try_with(|arena| callback(&mut arena.borrow_mut())) + } +} + +#[cfg(test)] +mod tests { + use std::{ + cell::{Cell, RefCell}, + path::PathBuf, + rc::Rc, + time::Duration, + }; + + use crate::{ + AnyWindowHandle, AppContext as _, Bounds, ContentMask, Context, DispatchPhase, + DragMoveEvent, Empty, ExternalDragPayload, ExternalPaths, FileDragPaths, FileDropEvent, + FocusHandle, InputEvent as _, InteractiveElement as _, IntoElement, KeyDownEvent, + Keystroke, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, + Pixels, PlatformInput, Point, Render, RequestFrameOptions, ScaledPixels, + StatefulInteractiveElement as _, Styled, TestAppContext, TouchDragEvent, TouchEvent, + TouchId, TouchPhase, Underline, UnderlineStyle, Window, WindowAppearance, WindowOptions, + canvas, div, hsla, point, px, size, + }; + + #[cfg(feature = "profiler")] + #[gpui::test] + fn test_frame_requests_and_skips_are_journaled(cx: &mut TestAppContext) { + use crate::FrameRequestSource; + use crate::profiler::journal::{ + self, ForegroundJournalEntry, FrameSkipReason, IntervalBoundary, + }; + use scheduler::Instant; + + cx.app.borrow_mut().mode = crate::app::GpuiMode::Production; + let window = cx.add_window(|_, _| EmptyView); + let platform_window = cx.test_window(window.into()); + platform_window.simulate_frame_request(RequestFrameOptions::default()); + let (journal, _guard) = journal::install_test_foreground_journal(64, 4); + let mut collector = journal.collector(); + + for (signal_at, source) in [ + (Some(Instant::now()), FrameRequestSource::NativeCallback), + (Some(Instant::now()), FrameRequestSource::LocalSchedule), + (None, FrameRequestSource::NativeCallback), + ] { + platform_window.simulate_frame_request(RequestFrameOptions { + signal_at, + signal_source: source, + require_presentation: true, + ..Default::default() + }); + let entries = collector.collect_unseen().entries; + let signal_index = entries + .iter() + .position(|entry| { + matches!( + entry, + ForegroundJournalEntry::PlatformSignal(signal) + if signal.window_id == window.window_id() + && signal.signal_at == signal_at + && signal.source == source + && signal_at.is_none_or(|at| signal.handled_at >= at) + ) + }) + .expect("frame request metadata"); + let skip_index = entries + .iter() + .position(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::FrameSkipped(skipped)) + if skipped.window_id == window.window_id() + && skipped.reason == FrameSkipReason::NoRenderNeeded + ) + }) + .expect("completed no-render boundary"); + assert!(signal_index < skip_index); + } + } + + #[cfg(feature = "profiler")] + #[gpui::test] + fn test_frame_demand_survives_throttling(cx: &mut TestAppContext) { + use crate::profiler::journal::{ + self, ForegroundJournalEntry, FrameSkipReason, IntervalBoundary, + }; + use scheduler::Instant; + + // Test mode draws during effect flushing, bypassing platform frame throttling. + cx.app.borrow_mut().mode = crate::app::GpuiMode::Production; + let (journal, _guard) = journal::install_test_foreground_journal(256, 4); + let mut collector = journal.collector(); + for initially_dirty in [true, false] { + // Keep the throttle closed without depending on short wall-clock deadlines. + let window = cx + .app + .borrow_mut() + .open_window( + WindowOptions { + inactive_frame_interval: Some(Duration::from_secs(3600)), + ..Default::default() + }, + |_, cx| cx.new(|_| EmptyView), + ) + .expect("open inactive test window"); + let platform_window = cx.test_window(window.into()); + platform_window.simulate_active_status_change(false); + platform_window.simulate_frame_request(RequestFrameOptions::default()); + let callback_ran = Rc::new(Cell::new(false)); + let draws_before = window + .update(cx, |_, window, _| { + let draws_before = window + .frame_duration_snapshot() + .draw_duration_histogram + .len(); + if initially_dirty { + window.refresh(); + } + window.on_next_frame({ + let callback_ran = callback_ran.clone(); + move |window, _| { + callback_ran.set(true); + window.refresh(); + } + }); + draws_before + }) + .expect("schedule throttled frame"); + // Skips only seal retained work, so give the throttled skip some. + let input_at = Instant::now(); + journal::record_input(journal::InputTiming { + kind: "test", + start: input_at, + end: input_at, + caused_invalidation: false, + }); + collector.collect_unseen(); + platform_window.simulate_frame_request(RequestFrameOptions { + signal_at: Some(Instant::now()), + ..Default::default() + }); + window + .update(cx, |_, window, _| { + assert_eq!( + window + .frame_duration_snapshot() + .draw_duration_histogram + .len(), + draws_before, + ); + }) + .expect("inspect deferred frame"); + assert!(!callback_ran.get()); + assert!(collector.collect_unseen().entries.iter().any(|entry| { + matches!( + entry, + ForegroundJournalEntry::Boundary(IntervalBoundary::FrameSkipped(skipped)) + if skipped.window_id == window.window_id() + && skipped.reason == FrameSkipReason::InactiveFrameRateLimit + ) + })); + platform_window.simulate_frame_request(RequestFrameOptions { + signal_at: Some(Instant::now()), + require_presentation: true, + ..Default::default() + }); + window + .update(cx, |_, window, _| { + assert_eq!( + window + .frame_duration_snapshot() + .draw_duration_histogram + .len(), + draws_before + 1, + ); + }) + .expect("inspect presented frame"); + assert!(callback_ran.get()); + } + } + + /// Visibility transitions reach observers exactly once each, with the new + /// state already stored on the window, and never wake the platform for a + /// frame: the platform requests one itself when it resumes presenting. + #[gpui::test] + fn test_window_visibility(cx: &mut TestAppContext) { + use crate::WindowVisibility; + + let window = cx.add_window(|_, _| EmptyView); + let observed = Rc::new(RefCell::new(Vec::new())); + let _subscription = window + .update(cx, { + let observed = observed.clone(); + move |_, window, _| { + assert_eq!(window.visibility(), WindowVisibility::Visible); + assert!(window.is_visible()); + window.observe_window_visibility(move |visibility, window, _| { + assert_eq!(window.visibility(), visibility); + observed.borrow_mut().push(visibility); + }) + } + }) + .unwrap(); + let test_window = cx.test_window(window.into()); + let frame_wake_count = test_window.frame_wake_count(); + + test_window.simulate_visibility_change(WindowVisibility::Hidden); + assert_eq!(*observed.borrow(), [WindowVisibility::Hidden]); + window + .update(cx, |_, window, _| assert!(!window.is_visible())) + .unwrap(); + + // Platforms may report the same state again; observers only see changes. + test_window.simulate_visibility_change(WindowVisibility::Hidden); + assert_eq!(observed.borrow().len(), 1); + + test_window.simulate_visibility_change(WindowVisibility::Visible); + assert_eq!( + *observed.borrow(), + [WindowVisibility::Hidden, WindowVisibility::Visible] + ); + window + .update(cx, |_, window, _| assert!(window.is_visible())) + .unwrap(); + assert_eq!(test_window.frame_wake_count(), frame_wake_count); + } + + #[gpui::test] + fn test_fully_visible_bounds_preserve_layout_viewport(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| EmptyView); + let mut platform_window = cx.test_window(window.into()); + platform_window.simulate_resize(size(px(400.), px(800.))); + window + .update(cx, |_, window, _| { + assert_eq!( + window.visual_viewport_bounds(), + Bounds::new(Point::default(), size(px(400.), px(800.))) + ); + assert_eq!( + window.fully_visible_bounds(), + window.visual_viewport_bounds() + ); + }) + .unwrap(); + + platform_window.simulate_frame_request(RequestFrameOptions::default()); + let wakes = platform_window.frame_wake_count(); + let visual_bounds = Bounds::new(point(px(10.), px(40.)), size(px(380.), px(460.))); + platform_window.simulate_visual_viewport_change(visual_bounds); + assert!(platform_window.frame_wake_count() > wakes); + platform_window.simulate_frame_request(RequestFrameOptions::default()); + let wakes = platform_window.frame_wake_count(); + platform_window.simulate_insets_change(crate::WindowInsets { + safe_area: crate::Edges { + top: px(60.), + right: px(20.), + bottom: px(30.), + left: px(-10.), + }, + ime: crate::Edges { + bottom: px(350.), + ..Default::default() + }, + }); + assert!(platform_window.frame_wake_count() > wakes); + window + .update(cx, |_, window, _| { + assert_eq!(window.viewport_size(), size(px(400.), px(800.))); + assert_eq!(window.visual_viewport_bounds(), visual_bounds); + assert_eq!( + window.fully_visible_bounds(), + Bounds::new(point(px(10.), px(60.)), size(px(370.), px(390.))) + ); + window.request_virtual_keyboard(); + window.dismiss_virtual_keyboard(); + assert_eq!(window.viewport_size(), size(px(400.), px(800.))); + }) + .unwrap(); + assert_eq!(platform_window.virtual_keyboard_requests(), 1); + assert_eq!(platform_window.virtual_keyboard_dismissals(), 1); + + platform_window.simulate_insets_change(crate::WindowInsets { + safe_area: crate::Edges { + top: px(900.), + left: px(500.), + ..Default::default() + }, + ..Default::default() + }); + window + .update(cx, |_, window, _| { + assert_eq!( + window.fully_visible_bounds().size, + size(Pixels::ZERO, Pixels::ZERO) + ); + }) + .unwrap(); + } + + struct EmptyView; + + impl Render for EmptyView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + } + } + + struct OpensWindowOnPaint { + opened: Rc>, + } + + impl Render for OpensWindowOnPaint { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let opened = self.opened.clone(); + div() + .size_full() + .child(canvas( + |_, _, _| {}, + move |_, _, _window, cx| { + if !opened.replace(true) { + cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| EmptyView)) + .unwrap(); + } + }, + )) + // Siblings painted after the canvas: their elements were + // allocated in the arena before the nested draw, so they detect + // a mid-draw arena clear when painted afterwards. + .child(div().child("after")) + } + } + + /// Opening a window synchronously draws it and requests an element arena + /// clear. When that happens from within another window's draw (here: from + /// an element's paint), the clear must be deferred until the outer draw + /// finishes, or the outer draw's arena-allocated elements would be freed + /// out from under it. + #[test] + fn test_window_opened_during_draw_defers_arena_clear() { + let mut cx = TestAppContext::single(); + + let opened = Rc::new(Cell::new(false)); + // add_window draws once, which runs the nested open_window mid-draw. + let window = cx.add_window({ + let opened = opened.clone(); + move |_, _| OpensWindowOnPaint { opened } + }); + + assert!(opened.get()); + assert_eq!(cx.windows().len(), 2); + + // The deferred clear must actually run once the outer draw unwinds: + // subsequent draws of both windows work against a fresh arena. + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + } + + #[test] + fn test_scale_factor_change_preserves_bounds_and_survives_resize() { + let mut cx = TestAppContext::single(); + let window = cx.add_window(|_, _| EmptyView); + let handle: AnyWindowHandle = window.into(); + let window_state = |cx: &mut TestAppContext| { + cx.update_window(handle, |_, window, _| { + ( + window.scale_factor(), + window.bounds(), + window.viewport_size(), + ) + }) + .unwrap() + }; + + let (scale_factor, mut expected_bounds, _) = window_state(&mut cx); + assert_eq!(scale_factor, 2.0); + + for (scale_factor, resized_size) in [ + (1.0, size(px(800.), px(600.))), + (1.25, size(px(640.), px(480.))), + (2.0, size(px(1024.), px(768.))), + ] { + cx.simulate_window_scale_factor_change(handle, scale_factor); + assert_eq!( + window_state(&mut cx), + (scale_factor, expected_bounds, expected_bounds.size) + ); + + cx.simulate_window_resize(handle, resized_size); + expected_bounds.size = resized_size; + assert_eq!( + window_state(&mut cx), + (scale_factor, expected_bounds, resized_size) + ); + } + } + + /// Platforms that stop requesting frames for idle windows (currently web) + /// rely on the frame waker firing whenever frame demand arises; a demand + /// source that skips the waker shows up there as a window that silently + /// stops repainting until unrelated activity wakes it. + #[gpui::test] + fn test_frame_waker_fires_on_frame_demand(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| EmptyView); + let test_window = cx.test_window(window.into()); + + // Windows start dirty, and that can predate waker installation; + // installing the waker must deliver the pending wake or the first + // frame would never be requested. + assert!( + test_window.frame_wake_count() >= 1, + "opening a window must wake the frame source for the initial frame" + ); + + // Serve outstanding demand (present the frame drawn by `add_window`). + test_window.simulate_frame_request(RequestFrameOptions::default()); + + // An idle window must not wake on clean frames or plain updates, or + // the frame source could never stop. + let baseline = test_window.frame_wake_count(); + test_window.simulate_frame_request(RequestFrameOptions::default()); + window.update(cx, |_, _, _| {}).unwrap(); + assert_eq!( + test_window.frame_wake_count(), + baseline, + "clean frames and non-notifying updates must not wake the frame source" + ); + + // Notifying a view in an idle window is the core demand signal. + window.update(cx, |_, _, cx| cx.notify()).unwrap(); + assert!( + test_window.frame_wake_count() > baseline, + "notifying a view in an idle window must wake the frame source" + ); + + // Serving that demand returns to idle without further wakes. + test_window.simulate_frame_request(RequestFrameOptions::default()); + let baseline = test_window.frame_wake_count(); + test_window.simulate_frame_request(RequestFrameOptions::default()); + assert_eq!( + test_window.frame_wake_count(), + baseline, + "serving demand must return the window to idle" + ); + + // Next-frame callbacks create demand without dirtying the window. + window + .update(cx, |_, window, _| window.on_next_frame(|_, _| {})) + .unwrap(); + assert!( + test_window.frame_wake_count() > baseline, + "scheduling a next-frame callback in an idle window must wake the frame source" + ); + } + + /// A frame request that arrives while next-frame callbacks are pending + /// must never strand them: either the frame runs them, or (when the + /// inactive-window frame-rate throttle defers the frame) the waker fires + /// so another request is delivered. + #[gpui::test] + fn test_pending_next_frame_callbacks_are_not_stranded(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| EmptyView); + let test_window = cx.test_window(window.into()); + // Establish a recent last-frame time so the inactive-window throttle + // can engage on the next request. + test_window.simulate_frame_request(RequestFrameOptions::default()); + + let callback_ran = Rc::new(Cell::new(false)); + window + .update(cx, { + let callback_ran = callback_ran.clone(); + move |_, window, _| { + window.on_next_frame(move |_, _| callback_ran.set(true)); + } + }) + .unwrap(); + + let baseline = test_window.frame_wake_count(); + test_window.simulate_frame_request(RequestFrameOptions::default()); + // The test window is inactive, so this request throttles to ~30fps + // when it lands within the throttle interval of the previous frame + // (the common case here, but timing-dependent): the callback is + // deferred and the waker must re-arm the frame source. On a slow run + // the request instead lands outside the interval and runs the + // callback directly. + assert!( + test_window.frame_wake_count() > baseline || callback_ran.get(), + "a frame request with pending next-frame callbacks must either run them or re-arm the frame source" + ); + } + + #[gpui::test] + fn test_window_reports_no_raw_handle_instead_of_panicking(cx: &mut TestAppContext) { + use raw_window_handle::{HandleError, HasDisplayHandle as _, HasWindowHandle as _}; + + let window = cx.add_window(|_, _| EmptyView); + window + .update(cx, |_, window, _| { + assert!(matches!( + window.window_handle(), + Err(HandleError::NotSupported) + )); + assert!(matches!( + window.display_handle(), + Err(HandleError::NotSupported) + )); + }) + .unwrap(); + } + + #[gpui::test] + fn test_appearance_change_runs_after_app_update(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| EmptyView); + let observed_appearance = Rc::new(Cell::new(None)); + let _subscription = window + .update(cx, { + let observed_appearance = observed_appearance.clone(); + move |_, window, _| { + window.observe_window_appearance(move |window, _| { + observed_appearance.set(Some(window.appearance())); + }) + } + }) + .unwrap(); + let test_window = cx.test_window(window.into()); + + cx.update(|_| { + test_window.simulate_appearance_change(WindowAppearance::Dark); + assert_eq!(observed_appearance.get(), None); + }); + cx.run_until_parked(); + + assert_eq!(observed_appearance.get(), Some(WindowAppearance::Dark)); + } + + #[gpui::test] + fn queued_frame_callback_wakes_a_parked_render_loop(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + assert!(test_window.simulate_scheduled_frame()); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + + cx.update_window(window.into(), |_, window, _| { + window.active.set(true); + window.on_next_frame(|_, _| {}); + }) + .unwrap(); + assert!( + test_window.frame_scheduled(), + "queuing work on a parked window must wake the render loop" + ); + + assert!(test_window.simulate_scheduled_frame()); + assert!( + test_window.frame_scheduled(), + "presenting the frame must await one compositor callback" + ); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + } + + #[gpui::test] + fn pending_presentation_wakes_a_parked_render_loop(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + assert!(test_window.simulate_scheduled_frame()); + assert!(test_window.simulate_scheduled_frame()); + assert!(!test_window.frame_scheduled()); + + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + + assert!( + test_window.frame_scheduled(), + "a rendered scene awaiting presentation must wake the render loop" + ); + } + + #[gpui::test] + fn callback_queued_during_a_frame_requests_a_follow_up(cx: &mut TestAppContext) { + let window = cx.add_window(|_, _| Empty); + let test_window = cx.test_window(window.into()); + + let callback_ran = Rc::new(Cell::new(false)); + cx.update_window(window.into(), |_, window, _| { + // Inactive windows are frame-rate throttled, which would defer the + // ticks this test drives manually. + window.active.set(true); + let callback_ran = callback_ran.clone(); + window.on_next_frame(move |window, _| { + window.on_next_frame(move |_, _| callback_ran.set(true)); + }); + }) + .unwrap(); + + assert!(test_window.simulate_scheduled_frame()); + assert!(!callback_ran.get()); + assert!( + test_window.frame_scheduled(), + "a callback queued mid-frame must schedule a follow-up before the loop parks" + ); + + assert!(test_window.simulate_scheduled_frame()); + assert!(callback_ran.get()); + } + + struct RootView { + explicit_size: bool, + child_bounds: Rc>>, + } + + impl Render for RootView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let child_bounds = self.child_bounds.clone(); + let root = div().flex().flex_col().child( + canvas( + move |bounds, _, _| child_bounds.set(bounds), + |_, _, _, _| {}, + ) + .size_full(), + ); + if self.explicit_size { + root.w(px(300.)).h(px(200.)) + } else { + root + } + } + } + + #[test] + fn auto_sized_window_root_fills_the_window() { + let mut cx = TestAppContext::single(); + let child_bounds = Rc::new(Cell::new(Bounds::default())); + let window = cx.add_window({ + let child_bounds = child_bounds.clone(); + move |_, _| RootView { + explicit_size: false, + child_bounds, + } + }); + + let viewport_size = cx + .update_window(window.into(), |_, window, cx| { + window.draw(cx).clear(cx); + window.viewport_size() + }) + .unwrap(); + + assert_eq!(child_bounds.get().size, viewport_size); + } + + #[test] + fn explicitly_sized_window_root_keeps_its_size() { + let mut cx = TestAppContext::single(); + let child_bounds = Rc::new(Cell::new(Bounds::default())); + let window = cx.add_window({ + let child_bounds = child_bounds.clone(); + move |_, _| RootView { + explicit_size: true, + child_bounds, + } + }); + + cx.update_window(window.into(), |_, window, cx| { + window.draw(cx).clear(cx); + }) + .unwrap(); + + assert_eq!(child_bounds.get().size, size(px(300.), px(200.))); + } + + struct FileDragView { + path: PathBuf, + observed_drag_moves: Rc>>>, + observed_drops: Rc>>, + } + + struct FileDropExitView(Rc>); + + impl Render for FileDropExitView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().size_full().on_file_drop_exit({ + let observed_file_drop_exit = self.0.clone(); + move |_, _, _| observed_file_drop_exit.set(observed_file_drop_exit.get() + 1) + }) + } + } + + impl Render for FileDragView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .id("file-drag") + .size_full() + .on_drag(self.path.clone(), |_, _, _, cx| cx.new(|_| Empty)) + .external_drag_payload(|path: &PathBuf, _, _| { + Some(ExternalDragPayload::Files(FileDragPaths::new([( + path.clone(), + true, + )]))) + }) + .on_drag_move({ + let observed_drag_moves = self.observed_drag_moves.clone(); + move |event: &DragMoveEvent, _, _| { + observed_drag_moves.borrow_mut().push(event.event.position); + } + }) + .on_drop({ + let observed_drops = self.observed_drops.clone(); + move |path: &PathBuf, _, _| observed_drops.borrow_mut().push(path.clone()) + }) + } + } + + #[gpui::test] + fn file_drag_is_promoted_once_and_restored_in_source_window(cx: &mut TestAppContext) { + struct Drag { + window: AnyWindowHandle, + observed_drag_moves: Rc>>>, + observed_drops: Rc>>, + } + + fn start_drag(cx: &mut TestAppContext, path: PathBuf, platform_result: bool) -> Drag { + let observed_drag_moves = Rc::new(RefCell::new(Vec::new())); + let observed_drops = Rc::new(RefCell::new(Vec::new())); + let window: AnyWindowHandle = cx + .add_window({ + let observed_drag_moves = observed_drag_moves.clone(); + let observed_drops = observed_drops.clone(); + move |_, _| FileDragView { + path, + observed_drag_moves, + observed_drops, + } + }) + .into(); + cx.test_window(window) + .set_start_external_drag_result(platform_result); + + let update_result = cx.update_window(window, |_, window, cx| { + window.draw(cx).clear(cx); + window.dispatch_event( + MouseDownEvent { + position: point(px(10.), px(10.)), + button: MouseButton::Left, + modifiers: Default::default(), + click_count: 1, + first_mouse: false, + } + .to_platform_input(), + cx, + ); + window.dispatch_event( + MouseMoveEvent { + position: point(px(20.), px(20.)), + pressed_button: Some(MouseButton::Left), + modifiers: Default::default(), + } + .to_platform_input(), + cx, + ); + assert!(cx.active_drag.is_some()); + }); + assert!( + update_result.is_ok(), + "failed to start drag: {update_result:?}" + ); + + assert!(cx.test_window(window).external_drag_files().is_empty()); + Drag { + window, + observed_drag_moves, + observed_drops, + } + } + + let successful_path = PathBuf::from("/tmp/successful-drag"); + let successful = start_drag(cx, successful_path.clone(), true); + let outside_position = point(px(-1.), px(20.)); + let update_result = cx.update_window(successful.window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: outside_position, + pressed_button: Some(MouseButton::Left), + modifiers: Default::default(), + } + .to_platform_input(), + cx, + ); + assert!(cx.active_drag.is_none()); + }); + assert!( + update_result.is_ok(), + "failed to promote drag: {update_result:?}" + ); + assert_eq!( + cx.test_window(successful.window).external_drag_files(), + [(successful_path.clone(), true)] + ); + // Views must still see the move that leaves the window, otherwise they never learn to tear + // down the drag state they built up while the pointer was inside. + assert_eq!( + successful.observed_drag_moves.borrow().last(), + Some(&outside_position) + ); + + let first_destination_exit_count = Rc::new(Cell::new(0)); + let first_destination: AnyWindowHandle = cx + .add_window({ + let first_destination_exit_count = first_destination_exit_count.clone(); + move |_, _| FileDropExitView(first_destination_exit_count) + }) + .into(); + let second_destination_exit_count = Rc::new(Cell::new(0)); + let second_destination: AnyWindowHandle = cx + .add_window({ + let second_destination_exit_count = second_destination_exit_count.clone(); + move |_, _| FileDropExitView(second_destination_exit_count) + }) + .into(); + let reentry_position = point(px(30.), px(30.)); + let external_paths = || ExternalPaths([successful_path.clone()].into_iter().collect()); + let update_result = cx.update_window(first_destination, |_, window, cx| { + window.draw(cx).clear(cx); + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: external_paths(), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); + assert!(cx.active_drag.is_none()); + assert_eq!(first_destination_exit_count.get(), 1); + assert_eq!(second_destination_exit_count.get(), 0); + }); + assert!( + update_result.is_ok(), + "failed to handle drag in first destination window: {update_result:?}" + ); + + let update_result = cx.update_window(second_destination, |_, window, cx| { + window.draw(cx).clear(cx); + window.dispatch_event( + PlatformInput::KeyDown(KeyDownEvent { + keystroke: Keystroke::parse("down").expect("valid keystroke"), + is_held: false, + prefer_character_input: false, + }), + cx, + ); + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: external_paths(), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + assert_eq!(first_destination_exit_count.get(), 1); + assert_eq!(second_destination_exit_count.get(), 0); + + window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); + assert!(cx.active_drag.is_none()); + assert_eq!(first_destination_exit_count.get(), 1); + assert_eq!(second_destination_exit_count.get(), 1); + }); + assert!( + update_result.is_ok(), + "failed to handle drag in second destination window: {update_result:?}" + ); + + let update_result = cx.update_window(successful.window, |_, window, cx| { + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: external_paths(), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + assert_eq!( + successful.observed_drag_moves.borrow().last(), + Some(&reentry_position) + ); + + window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); + assert!(cx.active_drag.is_none()); + + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: external_paths(), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + + window.dispatch_event( + FileDropEvent::Submit { + position: reentry_position, + } + .to_platform_input(), + cx, + ); + assert_eq!( + successful.observed_drops.borrow().as_slice(), + std::slice::from_ref(&successful_path) + ); + assert!(cx.active_drag.is_none()); + + window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); + assert!(cx.active_drag.is_none()); + window.dispatch_event(FileDropEvent::Ended.to_platform_input(), cx); + assert!(cx.active_drag.is_none()); + + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: external_paths(), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + window.dispatch_event(FileDropEvent::Exited.to_platform_input(), cx); + }); + assert!( + update_result.is_ok(), + "failed to restore drag in source window: {update_result:?}" + ); + + let cancelled_path = PathBuf::from("/tmp/cancelled-drag"); + let cancelled = start_drag(cx, cancelled_path.clone(), true); + let update_result = cx.update_window(cancelled.window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: outside_position, + pressed_button: Some(MouseButton::Left), + modifiers: Default::default(), + } + .to_platform_input(), + cx, + ); + assert!(cx.active_drag.is_none()); + + window.dispatch_event( + FileDropEvent::Entered { + position: reentry_position, + paths: ExternalPaths([cancelled_path].into_iter().collect()), + } + .to_platform_input(), + cx, + ); + assert!( + cx.active_drag + .as_ref() + .is_some_and(|drag| drag.value.downcast_ref::().is_some()) + ); + assert!(cx.stop_active_drag(window)); + assert!(cx.active_drag.is_none()); + }); + assert!( + update_result.is_ok(), + "failed to cancel restored drag: {update_result:?}" + ); + assert!(!cx.update(|cx| cx.end_platform_drag(cancelled.window.window_id()))); + + let removed_path = PathBuf::from("/tmp/removed-window-drag"); + let removed = start_drag(cx, removed_path, true); + let removed_window_id = removed.window.window_id(); + let update_result = cx.update_window(removed.window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: outside_position, + pressed_button: Some(MouseButton::Left), + modifiers: Default::default(), + } + .to_platform_input(), + cx, + ); + assert!(cx.active_drag.is_none()); + window.remove_window(); + }); + assert!( + update_result.is_ok(), + "failed to remove drag source window: {update_result:?}" + ); + assert!(!cx.update(|cx| cx.end_platform_drag(removed_window_id))); + + let failed_path = PathBuf::from("/tmp/failed-drag"); + let failed = start_drag(cx, failed_path.clone(), false); + let update_result = cx.update_window(failed.window, |_, window, cx| { + for x_position in [-1., -2.] { + window.dispatch_event( + MouseMoveEvent { + position: point(px(x_position), px(20.)), + pressed_button: Some(MouseButton::Left), + modifiers: Default::default(), + } + .to_platform_input(), + cx, + ); + } + assert!(cx.active_drag.is_some()); + }); + assert!( + update_result.is_ok(), + "failed to retain drag after platform failure: {update_result:?}" + ); + assert_eq!( + cx.test_window(failed.window).external_drag_files(), + [(failed_path, true)] + ); + } + + struct FocusForwarder { + a: FocusHandle, + b: FocusHandle, + } + + impl Render for FocusForwarder { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(div().w(px(50.)).h(px(50.)).track_focus(&self.a)) + .child(div().w(px(50.)).h(px(50.)).track_focus(&self.b)) + } + } + + /// When a focus listener moves focus again (e.g. a dock forwarding focus to its + /// active panel), the resulting focus events must be dispatched without waiting + /// for an unrelated redraw of the window. + #[gpui::test] + fn test_focus_moved_by_focus_listener_is_dispatched(cx: &mut TestAppContext) { + let b_focus_count = Rc::new(Cell::new(0)); + let window = cx.add_window({ + let b_focus_count = b_focus_count.clone(); + move |window, cx| { + let a = cx.focus_handle(); + let b = cx.focus_handle(); + cx.on_focus(&a, window, |this: &mut FocusForwarder, window, cx| { + let b = this.b.clone(); + window.focus(&b, cx); + }) + .detach(); + cx.on_focus(&b, window, move |_, _, _| { + b_focus_count.set(b_focus_count.get() + 1); + }) + .detach(); + FocusForwarder { a, b } + } + }); + + window + .update(cx, |_, window, _| window.activate_window()) + .unwrap(); + cx.executor().run_until_parked(); + + window + .update(cx, |this, window, cx| { + let a = this.a.clone(); + window.focus(&a, cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + + window + .update(cx, |this, window, _| { + assert!(this.b.is_focused(window)); + }) + .unwrap(); + assert_eq!(b_focus_count.get(), 1); + } + + #[gpui::test] + fn claimed_touch_drag_receives_movement_and_release(cx: &mut TestAppContext) { + let events = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let events = events.clone(); + move |_, _| TouchDragListener { events } + }); + let touch = TouchId(1); + + dispatch_touch(window, cx, touch, TouchPhase::Started, 10.); + dispatch_touch(window, cx, touch, TouchPhase::Moved, 30.); + dispatch_touch(window, cx, touch, TouchPhase::Ended, 40.); + + assert_eq!( + events.borrow().as_slice(), + [ + (TouchPhase::Started, px(10.)), + (TouchPhase::Moved, px(30.)), + (TouchPhase::Ended, px(40.)), + ] + ); + } + + struct TouchDragListener { + events: Rc>>, + } + + impl Render for TouchDragListener { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let events = self.events.clone(); + canvas( + |_, _, _| {}, + move |_, _, window, _| { + window.on_mouse_event(move |event: &TouchDragEvent, phase, window, _cx| { + if phase != DispatchPhase::Bubble { + return; + } + events.borrow_mut().push((event.phase, event.position.x)); + if event.phase == TouchPhase::Started { + window.prevent_default(); + } + }); + }, + ) + } + } + + #[gpui::test] + fn long_press_is_claimed_only_when_started_prevents_default(cx: &mut TestAppContext) { + for response in [ + LongPressResponse::PreventDefault, + LongPressResponse::StopPropagation, + LongPressResponse::None, + ] { + let phases = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let phases = phases.clone(); + move |_, _| LongPressListener { phases, response } + }); + dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); + cx.executor().advance_clock(Duration::from_millis(501)); + cx.executor().run_until_parked(); + window + .update(cx, |_, window, _| { + assert_eq!( + window.long_press_capture.is_some(), + response == LongPressResponse::PreventDefault + ); + }) + .unwrap(); + dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); + dispatch_touch(window, cx, TouchId(1), TouchPhase::Ended, 2.); + window + .update(cx, |_, window, _| { + assert!(window.long_press_capture.is_none()); + }) + .unwrap(); + + let phases = phases.borrow(); + if response == LongPressResponse::PreventDefault { + assert_eq!( + phases.as_slice(), + [TouchPhase::Started, TouchPhase::Moved, TouchPhase::Ended] + ); + } else { + assert_eq!(phases.as_slice(), [TouchPhase::Started]); + } + } + } + + #[gpui::test] + fn stale_default_prevention_does_not_claim_long_press(cx: &mut TestAppContext) { + let phases = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let phases = phases.clone(); + move |_, _| LongPressListener { + phases, + response: LongPressResponse::None, + } + }); + window + .update(cx, |_, window, _| { + window.prevent_default(); + }) + .unwrap(); + + dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); + cx.executor().advance_clock(Duration::from_millis(501)); + cx.executor().run_until_parked(); + dispatch_touch(window, cx, TouchId(1), TouchPhase::Moved, 2.); + + assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); + } + + #[gpui::test] + fn resolved_touch_cancels_scheduled_long_press(cx: &mut TestAppContext) { + for (phase, position) in [ + (TouchPhase::Ended, 0.), + (TouchPhase::Cancelled, 0.), + (TouchPhase::Moved, 20.), + ] { + let phases = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let phases = phases.clone(); + move |_, _| LongPressListener { + phases, + response: LongPressResponse::PreventDefault, + } + }); + dispatch_touch(window, cx, TouchId(1), TouchPhase::Started, 0.); + dispatch_touch(window, cx, TouchId(1), phase, position); + cx.executor().advance_clock(Duration::from_millis(501)); + cx.executor().run_until_parked(); + + assert!(phases.borrow().is_empty(), "{phase:?} allowed long press"); + } + } + + #[gpui::test] + fn stale_long_press_timer_cannot_affect_replacement_touch(cx: &mut TestAppContext) { + let phases = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let phases = phases.clone(); + move |_, _| LongPressListener { + phases, + response: LongPressResponse::PreventDefault, + } + }); + let first_touch = TouchId(1); + dispatch_touch(window, cx, first_touch, TouchPhase::Started, 0.); + cx.executor().advance_clock(Duration::from_millis(250)); + dispatch_touch(window, cx, first_touch, TouchPhase::Cancelled, 0.); + dispatch_touch(window, cx, TouchId(2), TouchPhase::Started, 10.); + + cx.executor().advance_clock(Duration::from_millis(251)); + cx.executor().run_until_parked(); + assert!(phases.borrow().is_empty()); + + cx.executor().advance_clock(Duration::from_millis(250)); + cx.executor().run_until_parked(); + assert_eq!(phases.borrow().as_slice(), [TouchPhase::Started]); + } + + #[gpui::test] + fn test_underline_exclusions_preserve_device_geometry(cx: &mut TestAppContext) { + test_underline_paint_at_scales(cx, |window| { + let (numerator, denominator) = match window.scale_factor() { + 1.0 => (1, 1), + 1.25 => (5, 4), + 1.5 => (3, 2), + 2.0 => (2, 1), + 3.0 => (3, 1), + scale => panic!("unexpected scale {scale}"), + }; + let snap = |quarters: i32| { + let scaled = quarters * numerator; + let divisor = 4 * denominator; + let rounded = + scaled.abs() / divisor + i32::from(2 * (scaled.abs() % divisor) > divisor); + ScaledPixels((scaled.signum() * rounded) as f32) + }; + let stroke = |quarters| { + if quarters == 0 { + ScaledPixels(0.) + } else { + snap(quarters).max(ScaledPixels(1.)) + } + }; + + for origin in [-101, -3, -2, -1, 0, 1, 2, 3, 101] { + for width in [0, 1, 3, 84] { + for thickness in [0, 1, 4, 7] { + for wavy in [false, true] { + let style = UnderlineStyle { + thickness: px(thickness as f32 / 4.), + color: Some(hsla(0.25, 0.5, 0.75, 0.5)), + wavy, + }; + let bounds = Bounds::new( + point(snap(origin), snap(-origin)), + size( + stroke(width), + stroke(thickness) * if wavy { 3. } else { 1. }, + ), + ); + let origin = point(px(origin as f32 / 4.), px(-origin as f32 / 4.)); + let width = px(width as f32 / 4.); + assert_eq!(window.underline_bounds(origin, width, &style), bounds); + let original = paint_test_underlines(window, |window| { + window.paint_underline(origin, width, &style); + }) + .to_vec(); + assert_eq!(original.len(), usize::from(!bounds.is_empty())); + if let Some(underline) = original.first() { + assert_eq!(underline.bounds, bounds); + assert_eq!(underline.thickness, stroke(thickness)); + assert_eq!(underline.wavy, wavy.into()); + assert_eq!(underline.color, hsla(0.25, 0.5, 0.75, 0.5)); + } + + let underlines = paint_test_underlines(window, |window| { + window.paint_underline_with_exclusions(origin, width, &style, &[]); + }); + assert_eq!(underlines.len(), original.len()); + for (actual, original) in underlines.iter().zip(&original) { + assert_same_underline_geometry(actual, original); + assert_eq!(actual.content_mask, original.content_mask); + } + + let left = bounds.left().0 as i32; + let right = bounds.right().0 as i32; + let exclusions = [ + (15, 25), + (3, 7), + (5, 10), + (10, 12), + (0, 0), + (4, 2), + (-50, -1), + (70, 100), + ] + .map(|(start, end)| { + ScaledPixels((left + start) as f32) + ..ScaledPixels((left + end) as f32) + }); + let underlines = paint_test_underlines(window, |window| { + window.paint_underline_with_exclusions( + origin, + width, + &style, + &exclusions, + ); + }); + let expected = (left..right) + .filter(|column| { + !bounds.is_empty() + && !exclusions.iter().any(|span| { + span.contains(&ScaledPixels(*column as f32)) + }) + }) + .collect::>(); + assert_eq!(underline_device_columns(underlines), expected); + for underline in underlines { + assert_same_underline_geometry(underline, &original[0]); + } + } + } + } + } + }); + } + + #[gpui::test] + fn test_underline_exclusions_preserve_helvetica_warning_edge(cx: &mut TestAppContext) { + test_underline_paint_at_scales(cx, |window| { + if window.scale_factor() != 2.0 { + return; + } + let style = UnderlineStyle { + thickness: px(1.), + color: None, + wavy: true, + }; + let origin = point(px(2.888_183_8), px(15.357_917)); + let width = px(23.848_145) - origin.x; + let original = paint_test_underlines(window, |window| { + window.paint_underline(origin, width, &style); + })[0]; + assert_eq!(original.bounds.left(), ScaledPixels(6.)); + assert_eq!(original.bounds.right(), ScaledPixels(48.)); + let point_bounds = window.underline_bounds(origin, px(10.829_102), &style); + assert_eq!(point_bounds.left(), ScaledPixels(6.)); + assert_eq!(point_bounds.right(), ScaledPixels(28.)); + + let underlines = paint_test_underlines(window, |window| { + window.paint_underline_with_exclusions( + origin, + width, + &style, + &[point_bounds.left()..point_bounds.right()], + ); + }); + assert_eq!(underlines.len(), 1); + assert_same_underline_geometry(&underlines[0], &original); + assert_eq!( + underline_device_columns(underlines), + (28..48).collect::>() + ); + }); + } + + #[gpui::test] + fn test_underline_exclusions_preserve_content_mask_and_opacity(cx: &mut TestAppContext) { + test_underline_paint_at_scales(cx, |window| { + let scale = window.scale_factor(); + let original_mask = window.content_mask(); + let original_opacity = window.element_opacity(); + let mask = ContentMask { + bounds: Bounds::from_corners(point(px(4.), px(10.)), point(px(16.), px(12.))), + }; + let style = UnderlineStyle { + thickness: px(2.), + color: Some(hsla(0.25, 0.5, 0.75, 0.5)), + wavy: true, + }; + let origin = point(px(-1.25), px(9.25)); + let width = px(24.); + window.with_content_mask(Some(mask), |window| { + window.with_element_opacity(Some(0.5), |window| { + window.with_element_opacity(Some(0.5), |window| { + let original = paint_test_underlines(window, |window| { + window.paint_underline(origin, width, &style); + })[0]; + assert_eq!(original.color, hsla(0.25, 0.5, 0.75, 0.125)); + let underlines = paint_test_underlines(window, |window| { + window.paint_underline_with_exclusions( + origin, + width, + &style, + &[ScaledPixels(6. * scale)..ScaledPixels(8. * scale)], + ); + }); + assert_eq!(underlines.len(), 2); + let expected = [(4. * scale, 6. * scale), (8. * scale, 16. * scale)].map( + |(left, right)| { + Bounds::from_corners( + point(ScaledPixels(left), ScaledPixels((10. * scale).floor())), + point(ScaledPixels(right), ScaledPixels((12. * scale).ceil())), + ) + }, + ); + assert_eq!( + underlines + .iter() + .map(|underline| underline.content_mask.bounds) + .collect::>(), + expected + ); + for underline in underlines { + assert_same_underline_geometry(underline, &original); + } + }); + }); + }); + assert_eq!(window.content_mask(), original_mask); + assert_eq!(window.element_opacity(), original_opacity); + }); + } + + #[gpui::test] + fn test_underline_exclusions_adversarial_permutations(cx: &mut TestAppContext) { + test_underline_paint_at_scales(cx, |window| { + let style = UnderlineStyle { + thickness: px(1.), + color: None, + wavy: true, + }; + let origin = point(px(-4.), px(-1.)); + let width = px(8.); + let left = (-4. * window.scale_factor()) as i32; + let right = (4. * window.scale_factor()) as i32; + let endpoints = [-16, -8, -1, 0, 1, 8, 16]; + for first_start in endpoints { + for first_end in endpoints { + for second_start in endpoints { + for second_end in endpoints { + let exclusions = [first_start..first_end, second_start..second_end]; + let expected = (left..right) + .filter(|column| { + !exclusions.iter().any(|span| span.contains(column)) + }) + .collect::>(); + let exclusions = exclusions.map(|span| { + ScaledPixels(span.start as f32)..ScaledPixels(span.end as f32) + }); + let underlines = paint_test_underlines(window, |window| { + window.paint_underline_with_exclusions( + origin, + width, + &style, + &exclusions, + ); + }); + assert_eq!( + underline_device_columns(underlines), + expected, + "{exclusions:?}" + ); + } + } + } + } + }); + } + + #[derive(Clone, Copy, PartialEq)] + enum LongPressResponse { + PreventDefault, + StopPropagation, + None, + } + + struct LongPressListener { + phases: Rc>>, + response: LongPressResponse, + } + + impl Render for LongPressListener { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let entity = cx.entity(); + let phases = self.phases.clone(); + let response = self.response; + canvas( + |_, _, _| {}, + move |_, _, window, _| { + window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| { + if phase != DispatchPhase::Bubble { + return; + } + phases.borrow_mut().push(event.phase); + match response { + LongPressResponse::PreventDefault => { + window.capture_long_press(&entity); + window.prevent_default(); + } + LongPressResponse::StopPropagation => cx.stop_propagation(), + LongPressResponse::None => {} + } + }); + }, + ) + } + } + + struct UnderlineTestView(Rc); + + impl Render for UnderlineTestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let paint = self.0.clone(); + canvas( + |_, _, _| {}, + move |_, _, window, _| { + window.content_mask_stack.push(ContentMask { + bounds: Bounds::from_corners( + point(px(-1000.), px(-1000.)), + point(px(1000.), px(1000.)), + ), + }); + paint(window); + window.content_mask_stack.pop(); + }, + ) + .size_full() + } + } + + fn test_underline_paint_at_scales( + cx: &mut TestAppContext, + paint: impl Fn(&mut Window) + 'static, + ) { + let window = cx.add_window(move |_, _| UnderlineTestView(Rc::new(paint))); + for scale in [1., 1.25, 1.5, 2., 3.] { + cx.simulate_window_scale_factor_change(window.into(), scale); + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + } + } + + fn paint_test_underlines(window: &mut Window, paint: impl FnOnce(&mut Window)) -> &[Underline] { + window.next_frame.scene.clear(); + paint(window); + &window.next_frame.scene.underlines + } + + fn assert_same_underline_geometry(actual: &Underline, expected: &Underline) { + assert_eq!(actual.bounds, expected.bounds); + assert_eq!(actual.thickness, expected.thickness); + assert_eq!(actual.wavy, expected.wavy); + assert_eq!(actual.color, expected.color); + assert_eq!(actual.pad, expected.pad); + } + + fn underline_device_columns(underlines: &[Underline]) -> Vec { + underlines + .iter() + .flat_map(|underline| { + let bounds = underline.bounds.intersect(&underline.content_mask.bounds); + bounds.left().0 as i32..bounds.right().0 as i32 + }) + .collect() + } + + fn dispatch_touch( + window: crate::WindowHandle, + cx: &mut TestAppContext, + id: TouchId, + phase: TouchPhase, + x: f32, + ) { + window + .update(cx, |_, window, cx| { + window.dispatch_event( + TouchEvent { + id, + phase, + position: point(px(x), px(0.)), + predicted_position: None, + force: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + } +} + +#[cfg(all(test, any(feature = "inspector", debug_assertions)))] +mod inspector_tests { + use super::*; + use crate::{ + DivInspectorState, InspectorElementId, MouseDownEvent, StyleRefinement, TestAppContext, div, + }; + + #[gpui::test] + fn inspector_only_tracks_its_open_window(cx: &mut TestAppContext) { + let windows = [ + cx.add_window(|_, cx| InspectorTestRoot { + child: cx.new(|_| InspectorTestView::default()), + }), + cx.add_window(|_, cx| InspectorTestRoot { + child: cx.new(|_| InspectorTestView::default()), + }), + ]; + for window in windows { + assert_closed_inspector(window.into(), cx); + } + for _ in 0..2 { + cx.update_window(windows[0].into(), |root, window, cx| { + let root = root.downcast::().expect("test root"); + let child_widths = root.read(cx).child.read(cx).child_widths.clone(); + window.toggle_inspector(cx); + window.draw(cx).clear(cx); + assert_eq!(child_widths.borrow().as_slice(), &[px(10.); 3]); + let path = window + .rendered_frame + .next_inspector_instance_ids + .iter() + .find_map(|(path, count)| (*count == 3).then(|| path.clone())) + .expect("anonymous siblings share an inspector path"); + let selected_id = InspectorElementId { + path, + instance_id: 1, + }; + let position = window + .rendered_frame + .hitboxes + .iter() + .find(|hitbox| { + window.rendered_frame.inspector_hitboxes.get(&hitbox.id) + == Some(&selected_id) + }) + .expect("middle sibling is pickable") + .bounds + .center(); + window.simulate_mouse_move(position, cx); + window.dispatch_event( + PlatformInput::MouseDown(MouseDownEvent { + position, + button: MouseButton::Left, + modifiers: Modifiers::default(), + click_count: 1, + first_mouse: false, + }), + cx, + ); + window.dispatch_event( + PlatformInput::MouseUp(MouseUpEvent { + position, + button: MouseButton::Left, + modifiers: Modifiers::default(), + click_count: 1, + }), + cx, + ); + assert!(!window.is_inspector_picking(cx)); + assert_eq!( + window + .inspector + .as_ref() + .expect("open inspector") + .read(cx) + .active_element_id(), + Some(&selected_id) + ); + window.draw(cx).clear(cx); + window.with_inspector_state::( + Some(&selected_id), + cx, + |state, _| { + let state = state.as_mut().expect("selected div has style state"); + assert_eq!( + state.base_style.size.width, + Some(crate::Length::from(px(10.))) + ); + state.base_style.size.width = Some(crate::Length::from(px(25.))); + }, + ); + window.refresh(); + window.draw(cx).clear(cx); + assert_eq!( + child_widths.borrow().as_slice(), + &[px(10.), px(25.), px(10.)] + ); + }) + .expect("pick and edit a cached child"); + assert_closed_inspector(windows[1].into(), cx); + windows[0] + .update(cx, |_, window, cx| window.toggle_inspector(cx)) + .expect("close inspector"); + assert_closed_inspector(windows[0].into(), cx); + } + } + + struct InspectorTestRoot { + child: Entity, + } + + impl Render for InspectorTestRoot { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.child + .clone() + .cached(StyleRefinement::default().size(px(100.))) + } + } + + #[derive(Default)] + struct InspectorTestView { + child_widths: Rc>>, + } + + impl Render for InspectorTestView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let child_widths = self.child_widths.clone(); + div() + .on_children_prepainted(move |bounds, _, _| { + *child_widths.borrow_mut() = + bounds.iter().map(|bounds| bounds.size.width).collect(); + }) + .with_dynamic_prepaint_order(|_, _| SmallVec::from_iter([2, 0, 1])) + .id("inspector-root") + .flex() + .children((0..3).map(|_| div().size(px(10.)).flex_shrink_0())) + } + } + + fn assert_closed_inspector(window: AnyWindowHandle, cx: &mut TestAppContext) { + cx.update_window(window, |root, window, cx| { + window.draw(cx).clear(cx); + window.draw(cx).clear(cx); + let root = root.downcast::().expect("test root"); + assert_eq!( + root.read(cx) + .child + .read(cx) + .child_widths + .borrow() + .as_slice(), + &[px(10.); 3] + ); + for frame in [&window.rendered_frame, &window.next_frame] { + assert_eq!(frame.next_inspector_instance_ids.capacity(), 0); + assert_eq!(frame.inspector_hitboxes.capacity(), 0); + } + }) + .expect("closed inspector has no bookkeeping and no style overrides"); + } +} diff --git a/vendor/gpui-pre/src/window/a11y.rs b/vendor/gpui-pre/src/window/a11y.rs new file mode 100644 index 0000000..5b63821 --- /dev/null +++ b/vendor/gpui-pre/src/window/a11y.rs @@ -0,0 +1,905 @@ +//! Accessibility support, provided by [AccessKit][accesskit]. +//! +//! There are user-facing guide-level docs [here](crate::_accessibility). +//! +//! ## Architecture +//! +//! ```text +//! ┌────────────────────────────────┐ ┌─────────────────────┐ +//! ┌─▶│ AccessKit Adapter (MacOS) │◀─▶│ MacOS System APIs │ +//! │ └────────────────────────────────┘ └─────────────────────┘ +//! │ +//! ┌──────┐ ┌───────────┐ │ ┌────────────────────────────────┐ ┌─────────────────────┐ +//! │ GPUI │◀─▶│ AccessKit │◀─┼─▶│ AccessKit Adapter (Windows) │◀─▶│ Windows System APIs │ +//! └──────┘ └───────────┘ │ └────────────────────────────────┘ └─────────────────────┘ +//! │ +//! │ ┌────────────────────────────────┐ ┌─────────────────────┐ +//! └─▶│ AccessKit Adapter (Linux) │◀─▶│ dbus │ +//! └────────────────────────────────┘ └─────────────────────┘ +//! ``` +//! +//! In order for GPUI apps to be usable for people using assistive technology, +//! we must do a few things: +//! - Inform the system when the UI changes meaningfully. This includes: +//! - Reporting new/removed/changed UI elements +//! - *Not* reporting irrelevant UI changes, e.g. an invisible `div()` being +//! added. +//! - Reporting the appearance and capabilities of each UI element. For example: +//! - What does this piece of text say? +//! - How far along is this progress bar? +//! - Can this node be focused? +//! - Can this node have a value directly assigned? (e.g. a slider) +//! - Allowing the system to interact with the UI by dispatching actions to +//! nodes. Note that AccessKit has its own [`Action`] type, which is not the +//! [`crate::Action`] trait. +//! - Activate and deactivate accessibility features when requested by the +//! system. +//! +//! Activating and deactivating at the right time is trivial, so I won't go into +//! detail here. The other two are almost orthogonal in implementation. +//! +//! The state for both lives in the [`A11y`] struct in this module. +//! +//! ### Reporting UI changes +//! +//! Every frame, we build a [`TreeUpdate`] and send it to the platform-specific +//! adapter. A [`TreeUpdate`] is a representation of a subset of the UI tree. +//! When the adapter receives the update, it diffs it against the previous +//! update, and calls platform-specific APIs to inform screen readers about the +//! changes. Nodes may have been created, destroyed, or updated. +//! +//! Each node has an ID, and this ID *should* be stable across frames. If a +//! node's ID changes, then, from AccessKit's point of view, it is a different +//! node. +//! +//! We derive the node ID from the [`GlobalElementId`] in +//! [`GlobalElementId::accesskit_node_id`]. Nodes without [`GlobalElementId`]s +//! cannot produce an AccessKit [`NodeId`], and so are not included in the +//! accessibility tree. We try to warn when using accessibility APIs on +//! [`div()`] without setting an ID. +//! +//! This all happens in [`Drawable::prepaint`]. The [`A11y`] struct maintains a +//! stack of nodes during prepainting, which we can use to calculate the +//! [`NodeId`]s, and record parent-child relationships. Once all [`Element`]s in +//! a frame have been prepainted, we send the resulting [`TreeUpdate`] object to +//! the adapter and the screen reader can announce the changes. +//! +//! #### Synthetic children +//! +//! Additionally, some nodes can register "synthetic children" using +//! [`Element::a11y_synthetic_children`]. Normally, one accesskit node is pushed +//! for every [`Element`] with a role and id. However, sometimes a single +//! element may want to produce many accesskit nodes. These extra nodes are +//! referred to as "synthetic children" of the element providing a non-default +//! [`Element::a11y_synthetic_children`] implementation. +//! +//! The user is provided a builder-style API using [`A11ySubtreeBuilder`], which +//! allows them to create push nodes that are children of the current node, as +//! well as modify the current node itself. +//! +//! GPUI calls this callback *after* prepainting (and just before popping the +//! corresponding element), since this step may need prepaint information to be +//! available. In the future, we may want to add prepaint information more +//! generally to [`Element::write_a11y_info`], but for now that's not necessary. +//! +//! ### Responding to actions +//! +//! On adapter creation, we provide a callback to the adapter, which can be used +//! to dispatch actions. This callback forwards to [`A11y::action_listeners`], a +//! mapping from [`NodeId`]s to action handlers (basically just `Box`). +//! +//! This is populated in: +//! - [`Window::on_a11y_action`], which is called by: +//! - [`Interactivity::paint`], which is called by: +//! - [`StatefulInteractiveElement::on_a11y_action`], which is a public-facing API +//! +//! These are cleared at the start of a frame, and re-populated during painting. +//! +//! [`NodeId`]: accesskit::NodeId + +use crate::*; + +pub(crate) mod debug; + +use crate::{App, Bounds, FocusId, Pixels, SharedString, Window}; +use accesskit::{Action, NodeId, TreeUpdate}; +use collections::{FxHashMap, FxHashSet}; +use smallvec::SmallVec; +use std::hash::{Hash, Hasher}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +/// The fixed AccessKit node ID used for the root of every window's a11y tree. +pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0); + +/// A listener for an accessibility action on a specific node. +pub(crate) type A11yActionListener = + Box, &mut Window, &mut App) + 'static>; + +/// Per-window accessibility state. +/// +/// Manages the AccessKit tree that is built each frame and the mappings +/// needed to dispatch incoming action requests back to the right elements. +pub(crate) struct A11y { + /// Whether accessibility has been [forcibly disabled] for this window. + /// + /// [forcibly disabled]: crate::Application::new_inaccessible + force_disabled: bool, + /// Whether a11y features have been requested by the system. + /// + /// Updated by AccessKit using callbacks provided to the adapter. Can change + /// halfway through a frame. + active_flag: Arc, + /// Whether a11y features are active for *this specific frame*. + /// + /// At the start of each frame, we load [`Self::active_flag`] (using + /// [`Self::sync_active_flag`]) and use this to determine whether we + /// should construct a [`TreeUpdate`] for this frame. It's important that + /// this value is stable within a frame, because the builder API exposed by + /// this type maintains a stack of nodes and each must be pushed and popped + /// exactly once. + /// + /// At the end of the frame, we re-call [`Self::sync_active_flag`] to + /// determine whether we should actually send the finished [`TreeUpdate`]. + active_this_frame: bool, + pub(crate) nodes: A11yNodeBuilder, + pub(crate) focus_ids: FxHashMap, + pub(crate) node_bounds: FxHashMap>, + pub(crate) action_listeners: FxHashMap>, + /// The window's title, used to label the root node so assistive + /// technology can tell windows apart. + window_title: Option, + /// The focus id we most recently reported as having no accessibility node, + /// used to log at most once per focus change rather than every frame. + last_focus_without_node: Option, + /// Retains the last tree update (and, in debug builds, per-node provenance) + /// so it can be dumped via [`crate::Window::debug_a11y_tree_json`]. + debug: debug::A11yDebug, + /// Maps a view's [`EntityId`] to its `Render` type name + #[cfg(debug_assertions)] + pub(crate) view_type_names: FxHashMap, +} + +impl A11y { + pub(crate) fn new( + active_flag: Arc, + force_disabled: bool, + window_title: Option, + ) -> Self { + Self { + force_disabled, + active_flag, + active_this_frame: false, + nodes: A11yNodeBuilder::new(), + focus_ids: FxHashMap::default(), + node_bounds: FxHashMap::default(), + action_listeners: FxHashMap::default(), + window_title, + last_focus_without_node: None, + debug: debug::A11yDebug::default(), + #[cfg(debug_assertions)] + view_type_names: FxHashMap::default(), + } + } + + /// Logs (once per focus change) that the focused element is not exposed to + /// assistive technology because it has no accessibility node. When this + /// happens, screen readers fall back to announcing the whole window instead + /// of the focused element. The fix is to give the element both an + /// `.id(...)` and a `.role(...)`. + pub(crate) fn note_focus_without_node(&mut self, focus_id: FocusId, reason: &str) { + if self.last_focus_without_node != Some(focus_id) { + self.last_focus_without_node = Some(focus_id); + log::info!( + "a11y: focused element ({focus_id:?}) has no accessibility node \ + ({reason}); assistive technology will announce the whole window \ + instead. Give it both an `.id(...)` and a `.role(...)` to expose it." + ); + } + } + + pub(crate) fn set_window_title(&mut self, title: impl Into) { + self.window_title = Some(title.into()); + } + + /// Ensures that [`Self::is_active`] returns up to date information. + /// + /// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`] + /// for more commentary. + pub(crate) fn sync_active_flag(&mut self) { + self.active_this_frame = self.is_enabled() && self.active_flag.load(Ordering::SeqCst); + } + + pub(crate) fn is_enabled(&self) -> bool { + !self.force_disabled + } + + pub(crate) fn is_active(&self) -> bool { + self.active_this_frame + } + + pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) { + self.focus_ids.insert(node_id, focus_id); + } + + /// Report `node_id` as the currently-focused node, if it is present in the + /// tree. + /// + /// Must only be called once per frame. + pub(crate) fn set_focus(&mut self, node_id: NodeId) { + // A focused node must have been registered as focusable this frame. + if !self.focus_ids.contains_key(&node_id) { + if cfg!(debug_assertions) { + panic!("set_focus called for a node that was not registered with set_focusable"); + } else { + log::warn!( + "a11y: set_focus called for a node that was not registered with \ + set_focusable ({node_id:?})" + ); + } + } + if self.nodes.has_node(node_id) { + // The focused element is properly exposed; reset the dedup so a + // later focus on a node-less element logs again. + self.last_focus_without_node = None; + self.nodes.set_focus(node_id); + } else { + // The element registered a focus handle and an id, but never got a + // node because it has no role. + if let Some(focus_id) = self.focus_ids.get(&node_id).copied() { + self.note_focus_without_node(focus_id, "it has an id but no role"); + } + } + } + + pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) { + // The active descendant must be a descendant of the focused container, + // not the focused node itself. + if self.nodes.node_is_focused(node_id) { + if cfg!(debug_assertions) { + panic!("set_active_descendant called on the focused node"); + } else { + log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})"); + } + return; + } + if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() { + self.nodes.set_active_descendant(node_id); + } + } + + /// Clear per-frame state and push the root node to start a new frame. + pub(crate) fn begin_frame(&mut self) { + self.focus_ids.clear(); + self.node_bounds.clear(); + self.action_listeners.clear(); + self.nodes.begin_frame(self.window_title.as_ref()); + } + + /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter. + pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate { + let update = self.nodes.finalize(); + self.debug.capture( + &update, + self.nodes.focus, + self.nodes.active_descendant, + self.window_title.as_ref(), + frame, + ); + #[cfg(debug_assertions)] + self.debug.capture_node_info(&self.nodes.node_info); + update + } + + pub(crate) fn debug_tree_json(&self) -> Option { + self.debug.to_json() + } +} + +/// Builder API for synthetic children. See the docs for +/// [`Element::a11y_synthetic_children`]. +pub struct A11ySubtreeBuilder<'a> { + parent_id: NodeId, + nodes: &'a mut A11yNodeBuilder, + /// Provenance of the real element whose `a11y_synthetic_children` is + /// running. + #[cfg(debug_assertions)] + creator: debug::NodeCreator, +} + +impl<'a> A11ySubtreeBuilder<'a> { + pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self { + Self { + parent_id, + nodes, + #[cfg(debug_assertions)] + creator: debug::NodeCreator::default(), + } + } + + #[cfg(debug_assertions)] + pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self { + self.creator = creator; + self + } + + /// Derive a [`NodeId`] for a synthetic child. + /// + /// The generated ID is based on the hash of `key`, as well as the parent's + /// ID. This means that `key`s must be unique within the same + /// [`Element::a11y_synthetic_children`] call, but may be duplicated across + /// different calls. + pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId { + let mut hasher = std::hash::DefaultHasher::default(); + self.parent_id.0.hash(&mut hasher); + key.hash(&mut hasher); + NodeId(hasher.finish()) + } + + /// Append a synthetic leaf node as a child of this element's node. + /// + /// Returns `false` if a node with this id is already present in the tree, + /// in which case the node is discarded. + pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool { + let pushed = self.nodes.push_leaf(id, node); + #[cfg(debug_assertions)] + if pushed { + self.nodes.record_node_info( + id, + debug::NodeDebugInfo { + synthetic: true, + view: self.creator.view, + element_id: self.creator.element_id.clone(), + source_location: self.creator.source_location, + }, + ); + } + pushed + } + + /// A mutable reference to the parent node. + pub fn parent_node(&mut self) -> &mut accesskit::Node { + self.nodes + .current_node_mut() + .expect("A11ySubtreeBuilder exists only while its element's node is on the stack") + } +} + +pub(crate) struct A11yNodeBuilder { + ids_stack: SmallVec<[NodeId; 16]>, + nodes_stack: SmallVec<[accesskit::Node; 16]>, + /// This is the exact type required by accesskit, so we can't just make it a + /// `HashMap` to remove the need for `seen_ids` + all_nodes: Vec<(NodeId, accesskit::Node)>, + seen_ids: FxHashSet, + /// The node that GPUI considers focused. Note that this may be different to + /// what is reported to accesskit - see [`Self::active_descendant`] + focus: Option, + /// If a node calls `.aria_active_descendant()`, AND an ancestor is focused, + /// override it as the focused node. This supports the "active descendant" + /// pattern, which allows a focused container to act as if a descendant is + /// focused. + active_descendant: Option, + #[cfg(debug_assertions)] + node_info: FxHashMap, +} + +impl A11yNodeBuilder { + fn new() -> Self { + Self { + ids_stack: SmallVec::new(), + nodes_stack: SmallVec::new(), + all_nodes: Vec::new(), + seen_ids: FxHashSet::default(), + focus: None, + active_descendant: None, + #[cfg(debug_assertions)] + node_info: FxHashMap::default(), + } + } + + /// Records provenance for a node already pushed this frame. Debug builds only. + #[cfg(debug_assertions)] + pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) { + self.node_info.insert(id, info); + } + + #[must_use] + fn can_push(&mut self, id: NodeId) -> bool { + debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root"); + + if !self.seen_ids.insert(id) { + debug_assert!( + false, + "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree." + ); + return false; + } + + true + } + + /// Push a new node onto the stack. It becomes a child of the current + /// top-of-stack node. + /// + /// Returns `true` if the node was successfully pushed. + pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool { + if !self.can_push(id) { + return false; + } + + if let Some(parent) = self.nodes_stack.last_mut() { + parent.push_child(id); + } + self.ids_stack.push(id); + self.nodes_stack.push(node); + true + } + + /// Add a leaf node as a child of the current top-of-stack node, without + /// pushing it onto the stack. Semantically equivalent to a [`Self::push`] + /// followed by a [`Self::pop`]. + /// + /// Returns `true` if the node was successfully pushed. + pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool { + if !self.can_push(id) { + return false; + } + + if let Some(parent) = self.nodes_stack.last_mut() { + parent.push_child(id); + } + self.all_nodes.push((id, node)); + true + } + + pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> { + self.nodes_stack.last_mut() + } + + /// Pop the current node off the stack and finalize it into the all_nodes + /// list. + pub(crate) fn pop(&mut self) { + debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node"); + + if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) { + self.all_nodes.push((id, node)); + } + } + + /// Push the root node to start a new frame. + fn begin_frame(&mut self, window_title: Option<&SharedString>) { + self.all_nodes.clear(); + self.ids_stack.clear(); + self.nodes_stack.clear(); + self.seen_ids.clear(); + #[cfg(debug_assertions)] + self.node_info.clear(); + let mut root_node = accesskit::Node::new(accesskit::Role::Window); + if let Some(title) = window_title { + root_node.set_label(title.to_string()); + } + + self.ids_stack.push(ROOT_NODE_ID); + self.nodes_stack.push(root_node); + self.focus = None; + self.active_descendant = None; + } + + /// Returns whether a node with the given ID has been pushed in this frame. + pub(crate) fn has_node(&self, id: NodeId) -> bool { + id == ROOT_NODE_ID || self.seen_ids.contains(&id) + } + + /// Returns whether `id` is the node currently reported as focused. + pub(crate) fn node_is_focused(&self, id: NodeId) -> bool { + self.focus == Some(id) + } + + pub(crate) fn focus_is_ancestor_of_current(&self) -> bool { + let Some(focus) = self.focus else { + return false; + }; + + // The current node is on top of the stack; everything below it is an + // ancestor. + let ancestor_count = self.ids_stack.len().saturating_sub(1); + self.ids_stack[..ancestor_count].contains(&focus) + } + + pub(crate) fn set_active_descendant(&mut self, id: NodeId) { + if self + .active_descendant + .is_some_and(|existing| existing != id) + { + if cfg!(debug_assertions) { + panic!("active descendant claimed by multiple nodes in one frame"); + } else { + log::warn!( + "a11y: multiple nodes claimed the active descendant this frame; \ + using last-wins ({id:?})" + ); + } + } + self.active_descendant = Some(id); + } + + pub(crate) fn set_focus(&mut self, id: NodeId) { + if self.focus.is_some() { + if cfg!(debug_assertions) { + panic!("set_focus called more than once in a single frame"); + } else { + log::warn!( + "a11y: set_focus called more than once in a single frame; \ + using last-wins ({id:?})" + ); + } + } + self.focus = Some(id); + } + + fn finalize(&mut self) -> TreeUpdate { + // Stack should contain only the root node + debug_assert_eq!(self.ids_stack.len(), 1); + debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID); + + if self.ids_stack.len() != 1 { + log::error!( + "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \ + Some elements may have pushed without popping.", + self.ids_stack.len() + ); + } + + // Pop remaining nodes (should just be the root). + while !self.ids_stack.is_empty() { + if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) { + self.all_nodes.push((id, node)); + } + } + + let focus = match self.active_descendant { + Some(id) if self.has_node(id) => id, + Some(id) => { + if cfg!(debug_assertions) { + panic!("active_descendant set to {id:?}, which is not in the tree"); + } else { + log::warn!("active_descendant set to {id:?}, which is not in the tree"); + self.focus.unwrap_or(ROOT_NODE_ID) + } + } + + _ => self.focus.unwrap_or(ROOT_NODE_ID), + }; + + let nodes = std::mem::take(&mut self.all_nodes); + let update = TreeUpdate { + nodes, + tree: Some(accesskit::Tree::new(ROOT_NODE_ID)), + tree_id: accesskit::TreeId::ROOT, + focus, + }; + + Self::repair_tree_update(update) + } + + /// Accesskit panics on invalid [`TreeUpdate`]s. This function defensively + /// checks invariants that accesskit panics on, and tries to fix them. + fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate { + let node_ids: FxHashSet = update.nodes.iter().map(|(id, _)| *id).collect(); + + // Focus must point to a node in the tree. + if !node_ids.contains(&update.focus) { + log::error!( + "a11y: Focused node {:?} is not in the tree ({} nodes). \ + Falling back to root. This is a bug in the a11y tree builder.", + update.focus, + update.nodes.len() + ); + update.focus = ROOT_NODE_ID; + } + + // Every child reference must point to a node in the update. + for (id, node) in &mut update.nodes { + let has_invalid_child = node + .children() + .iter() + .any(|child_id| !node_ids.contains(child_id)); + if has_invalid_child { + let children = node.children(); + let invalid_count = children + .iter() + .filter(|child_id| !node_ids.contains(child_id)) + .count(); + log::error!( + "a11y: Node {:?} references {} children not present in the tree. \ + Stripping invalid child references.", + id, + invalid_count + ); + let valid: Vec = children + .iter() + .copied() + .filter(|child_id| node_ids.contains(child_id)) + .collect(); + node.set_children(valid); + } + } + + update + } +} + +#[cfg(test)] +mod tests { + // Import specific items rather than glob-importing `super`, which would pull + // in gpui's own `test` attribute macro and shadow the standard one. + use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID}; + use crate::FocusId; + use accesskit::{NodeId, Role}; + use std::sync::{Arc, atomic::AtomicBool}; + + fn test_node() -> accesskit::Node { + accesskit::Node::new(Role::GenericContainer) + } + + fn new_builder() -> A11yNodeBuilder { + let mut builder = A11yNodeBuilder::new(); + builder.begin_frame(None); + builder + } + + fn new_a11y() -> A11y { + let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None); + a11y.begin_frame(); + a11y + } + + #[test] + fn accessibility_enabled_is_independent_of_activation() { + for force_disabled in [false, true] { + for active in [false, true] { + let mut a11y = A11y::new(Arc::new(AtomicBool::new(active)), force_disabled, None); + a11y.sync_active_flag(); + + assert_eq!(a11y.is_enabled(), !force_disabled); + assert_eq!(a11y.is_active(), !force_disabled && active); + } + } + } + + #[test] + fn active_descendant_honored_when_container_focused() { + let mut builder = new_builder(); + let container = NodeId(1); + let item = NodeId(2); + + assert!(builder.push(container, test_node())); + builder.set_focus(container); + assert!(builder.push(item, test_node())); + + // The item is on top of the stack; the focused container is its + // ancestor, so the claim is honored. + assert!(builder.focus_is_ancestor_of_current()); + builder.set_active_descendant(item); + + builder.pop(); // item + builder.pop(); // container + let update = builder.finalize(); + assert_eq!(update.focus, item); + } + + #[test] + fn active_descendant_honored_for_deep_descendant() { + let mut builder = new_builder(); + let container = NodeId(1); + let group = NodeId(2); + let item = NodeId(3); + + assert!(builder.push(container, test_node())); + builder.set_focus(container); + assert!(builder.push(group, test_node())); + assert!(builder.push(item, test_node())); + + // The item is a grandchild of the focused container; depth doesn't + // matter, the focused ancestor is still on the stack. + assert!(builder.focus_is_ancestor_of_current()); + builder.set_active_descendant(item); + + builder.pop(); // item + builder.pop(); // group + builder.pop(); // container + let update = builder.finalize(); + assert_eq!(update.focus, item); + } + + #[test] + fn active_descendant_ignored_when_focus_in_other_subtree() { + let mut builder = new_builder(); + let focused_container = NodeId(1); + let focused_leaf = NodeId(2); + let other_container = NodeId(3); + let other_item = NodeId(4); + + // First subtree holds real focus. + assert!(builder.push(focused_container, test_node())); + assert!(builder.push(focused_leaf, test_node())); + builder.set_focus(focused_leaf); + builder.pop(); // focused_leaf + builder.pop(); // focused_container + + // Second subtree: its item would claim the active descendant, but the + // focus is not on any of its ancestors, so the gate rejects it. + assert!(builder.push(other_container, test_node())); + assert!(builder.push(other_item, test_node())); + assert!(!builder.focus_is_ancestor_of_current()); + builder.pop(); // other_item + builder.pop(); // other_container + + let update = builder.finalize(); + assert_eq!(update.focus, focused_leaf); + } + + #[test] + fn active_descendant_ignored_when_nothing_focused() { + let mut builder = new_builder(); + let container = NodeId(1); + let item = NodeId(2); + + assert!(builder.push(container, test_node())); + assert!(builder.push(item, test_node())); + + // Nothing is focused (focus defaults to the root window node), so the + // gate rejects the claim. + assert!(!builder.focus_is_ancestor_of_current()); + builder.pop(); + builder.pop(); + + let update = builder.finalize(); + assert_eq!(update.focus, ROOT_NODE_ID); + } + + #[test] + fn regular_focus_used_when_no_active_descendant() { + let mut builder = new_builder(); + let focused = NodeId(1); + + assert!(builder.push(focused, test_node())); + builder.set_focus(focused); + builder.pop(); + + let update = builder.finalize(); + assert_eq!(update.focus, focused); + } + + #[test] + fn focus_is_ancestor_excludes_self_and_non_ancestors() { + let mut builder = new_builder(); + let container = NodeId(1); + let item = NodeId(2); + + assert!(builder.push(container, test_node())); + builder.set_focus(container); + + // With the focused container itself on top, it is not its own (strict) + // ancestor, so the gate is false. + assert!(!builder.focus_is_ancestor_of_current()); + + assert!(builder.push(item, test_node())); + // Now the focused container is a strict ancestor of the item on top. + assert!(builder.focus_is_ancestor_of_current()); + + builder.pop(); + builder.pop(); + } + + // The double-claim guard panics only in debug builds; in release it falls + // back to last-wins with a warning. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "active descendant claimed by multiple nodes") + )] + fn multiple_active_descendant_claims_panic_in_debug() { + let mut builder = new_builder(); + builder.set_active_descendant(NodeId(1)); + builder.set_active_descendant(NodeId(2)); + } + + // Setting focus twice in one frame means two elements both claimed window + // focus; that panics in debug and falls back to last-wins in release. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "set_focus called more than once") + )] + fn setting_focus_twice_panics_in_debug() { + let mut builder = new_builder(); + builder.set_focus(NodeId(1)); + builder.set_focus(NodeId(2)); + } + + // Focusing a node that was never registered as focusable is a bug: panic in + // debug, warn in release. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "was not registered with set_focusable") + )] + fn set_focus_without_set_focusable() { + let mut a11y = new_a11y(); + let node = NodeId(1); + assert!(a11y.nodes.push(node, test_node())); + // set_focusable was never called for `node`. + a11y.set_focus(node); + } + + // The focused node cannot also be its own active descendant: panic in + // debug, warn in release. + #[test] + #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))] + fn set_active_descendant_on_focused_node() { + let mut a11y = new_a11y(); + let node = NodeId(1); + assert!(a11y.nodes.push(node, test_node())); + a11y.set_focusable(node, FocusId::default()); + a11y.set_focus(node); + a11y.set_active_descendant(node); + } + + // Two sibling children of a focused container both claim the active + // descendant (both pass the focus gate). The second claim is a bug: panic + // in debug, last-wins + warn in release. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "active descendant claimed by multiple nodes") + )] + fn two_siblings_claiming_active_descendant() { + let mut a11y = new_a11y(); + let container = NodeId(1); + let first = NodeId(2); + let second = NodeId(3); + + assert!(a11y.nodes.push(container, test_node())); + a11y.set_focusable(container, FocusId::default()); + a11y.set_focus(container); + + assert!(a11y.nodes.push(first, test_node())); + a11y.set_active_descendant(first); + a11y.nodes.pop(); // first + + assert!(a11y.nodes.push(second, test_node())); + a11y.set_active_descendant(second); + a11y.nodes.pop(); // second + + a11y.nodes.pop(); // container + } + + // Node A is focused; node C (a child of the unfocused node B) claims the + // active descendant. The final tree must still report A as focused. + #[test] + fn active_descendant_in_unfocused_subtree_keeps_real_focus() { + let mut a11y = new_a11y(); + let a = NodeId(1); + let b = NodeId(2); + let c = NodeId(3); + + assert!(a11y.nodes.push(a, test_node())); + a11y.set_focusable(a, FocusId::default()); + a11y.set_focus(a); + a11y.nodes.pop(); // a + + assert!(a11y.nodes.push(b, test_node())); + assert!(a11y.nodes.push(c, test_node())); + a11y.set_active_descendant(c); + a11y.nodes.pop(); // c + a11y.nodes.pop(); // b + + let update = a11y.end_frame(Default::default()); + assert_eq!(update.focus, a); + } +} diff --git a/vendor/gpui-pre/src/window/a11y/debug.rs b/vendor/gpui-pre/src/window/a11y/debug.rs new file mode 100644 index 0000000..ae98aa5 --- /dev/null +++ b/vendor/gpui-pre/src/window/a11y/debug.rs @@ -0,0 +1,330 @@ +//! Developer tooling for inspecting the accessibility tree. +//! +//! [`A11yDebug`] retains the last [`TreeUpdate`] sent to the platform adapter so +//! it can be serialized on demand (see +//! [`crate::Window::debug_a11y_tree_json`]). In `cfg(debug_assertions)` builds, +//! we capture extra info. + +use accesskit::{Action, NodeId, TreeUpdate}; +use collections::FxHashMap; + +use crate::{Pixels, SharedString, Size}; + +#[derive(Default)] +pub(crate) struct FrameDebugInfo { + pub viewport_size: Size, + pub scale_factor: f32, + pub tab_stop_count: usize, +} + +struct CapturedFrame { + rendered_at: String, + frame_number: u64, + window_title: Option, + node_count: usize, + tab_stop_count: usize, + viewport_size: Size, + scale_factor: f32, +} + +#[cfg(debug_assertions)] +#[derive(Clone, Default)] +pub(crate) struct NodeDebugInfo { + /// Whether the node was synthesized via + /// [`crate::Element::a11y_synthetic_children`] rather than created from a + /// real element with a role and ID. + pub synthetic: bool, + /// The type name of the `Render` view that was rendering when the node was + /// created. + pub view: Option<&'static str>, + /// The [`ElementId`](crate::ElementId) of the creating element (the leaf of + /// its `GlobalElementId`, not the full path). For a synthetic node, this is + /// the real element whose `a11y_synthetic_children` produced it. + pub element_id: Option, + /// Source location where the creating element was constructed. + pub source_location: Option<&'static core::panic::Location<'static>>, +} + +#[cfg(debug_assertions)] +#[derive(Clone, Default)] +pub(crate) struct NodeCreator { + pub view: Option<&'static str>, + pub element_id: Option, + pub source_location: Option<&'static core::panic::Location<'static>>, +} + +#[derive(Default)] +pub(crate) struct A11yDebug { + last_tree_update: Option, + last_gpui_focus: Option, + last_active_descendant: Option, + /// Monotonic counter incremented on each captured frame, so a re-dump makes + /// it obvious whether the tree actually refreshed. + frame_number: u64, + /// Metadata about the most recently captured frame. + last_frame: Option, + #[cfg(debug_assertions)] + last_node_info: FxHashMap, +} + +impl A11yDebug { + pub(crate) fn capture( + &mut self, + update: &TreeUpdate, + gpui_focus: Option, + active_descendant: Option, + window_title: Option<&SharedString>, + frame: FrameDebugInfo, + ) { + self.last_tree_update = Some(update.clone()); + self.last_gpui_focus = gpui_focus; + self.last_active_descendant = active_descendant; + self.frame_number += 1; + self.last_frame = Some(CapturedFrame { + rendered_at: chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, false), + frame_number: self.frame_number, + window_title: window_title.cloned(), + node_count: update.nodes.len(), + tab_stop_count: frame.tab_stop_count, + viewport_size: frame.viewport_size, + scale_factor: frame.scale_factor, + }); + } + + #[cfg(debug_assertions)] + pub(crate) fn capture_node_info(&mut self, node_info: &FxHashMap) { + self.last_node_info = node_info.clone(); + } + + /// Serialize the last tree update to a readable JSON string. Node ids are + /// replaced with short ephemeral ids (`a`, `b`, ..., `z`, `aa`, ...). + pub(crate) fn to_json(&self) -> Option { + let update = self.last_tree_update.as_ref()?; + + let mut ephemeral: FxHashMap = FxHashMap::default(); + for (index, (id, _)) in update.nodes.iter().enumerate() { + ephemeral.insert(*id, ephemeral_id(index)); + } + + let mut nodes = serde_json::Map::new(); + for (id, node) in &update.nodes { + let key = ephemeral + .get(id) + .cloned() + .unwrap_or_else(|| id.0.to_string()); + #[cfg(debug_assertions)] + let provenance = self + .last_node_info + .get(id) + .map(|info| NodeProvenance { + element_id: info.element_id.as_deref(), + view: info.view, + source_location: info.source_location.map(|loc| loc.to_string()), + // Only surface synthetic nodes; `false` is the default and + // would just be noise on every real node. + synthetic: info.synthetic.then_some(true), + }) + .unwrap_or_default(); + #[cfg(not(debug_assertions))] + let provenance = NodeProvenance::default(); + let value = node_to_json(*id, node, &ephemeral, &provenance); + nodes.insert(key, value); + } + + let frame = self.last_frame.as_ref().map(|frame| { + serde_json::json!({ + "rendered_at": frame.rendered_at, + "frame_number": frame.frame_number, + "window_title": frame.window_title.as_ref().map(|title| title.to_string()), + "node_count": frame.node_count, + "tab_stop_count": frame.tab_stop_count, + "viewport_size": { + "width": frame.viewport_size.width.0, + "height": frame.viewport_size.height.0, + }, + "scale_factor": frame.scale_factor, + }) + }); + + let root = update + .tree + .as_ref() + .map(|tree| tree.root) + .and_then(|id| ephemeral.get(&id).cloned()); + + let value = serde_json::json!({ + "root": root, + "gpui_focus": self.last_gpui_focus.and_then(|id| ephemeral.get(&id).cloned()), + "active_descendant_focus": self.last_active_descendant.and_then(|id| ephemeral.get(&id).cloned()), + "frame": frame, + "nodes": nodes, + }); + Some(serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string())) + } +} + +#[derive(Default)] +struct NodeProvenance<'a> { + element_id: Option<&'a str>, + view: Option<&'a str>, + source_location: Option, + synthetic: Option, +} + +fn node_to_json( + id: NodeId, + node: &accesskit::Node, + ephemeral: &FxHashMap, + provenance: &NodeProvenance, +) -> serde_json::Value { + use serde_json::json; + + let mut map = serde_json::Map::new(); + map.insert("accesskit_id".into(), json!(id.0.to_string())); + + let children: Vec = node + .children() + .iter() + .map(|child| { + ephemeral + .get(child) + .cloned() + .unwrap_or_else(|| child.0.to_string()) + }) + .collect(); + if !children.is_empty() { + map.insert("children".into(), json!(children)); + } + + // Provenance (debug builds only), ordered before the accessibility section. + if let Some(element_id) = provenance.element_id { + map.insert("element_id".into(), json!(element_id)); + } + if let Some(view) = provenance.view { + map.insert("view".into(), json!(view)); + } + if let Some(source_location) = &provenance.source_location { + map.insert("source_location".into(), json!(source_location)); + } + if let Some(synthetic) = provenance.synthetic { + map.insert("synthetic".into(), json!(synthetic)); + } + + // Accessibility semantics for this node, grouped together. + let mut aria = serde_json::Map::new(); + aria.insert("role".into(), json!(format!("{:?}", node.role()))); + + // Which action types the node supports. AccessKit keeps these in a private + // bitset with no getter or iterator, so we probe each variant. `Action::n` + // (from AccessKit's `enumn` feature) maps a discriminant to its variant, + // returning `None` past the last one - so this can't drift out of sync with + // AccessKit's `Action` enum the way a hand-maintained list would. + let mut next_action = 0u8; + let on_action: Vec = std::iter::from_fn(move || { + let action = Action::n(next_action)?; + next_action += 1; + Some(action) + }) + .filter(|action| node.supports_action(*action)) + .map(|action| format!("{action:?}")) + .collect(); + if !on_action.is_empty() { + aria.insert("on_action".into(), json!(on_action)); + } + + // String properties. + if let Some(v) = node.label() { + aria.insert("label".into(), json!(v)); + } + if let Some(v) = node.description() { + aria.insert("description".into(), json!(v)); + } + if let Some(v) = node.value() { + aria.insert("value".into(), json!(v)); + } + if let Some(v) = node.keyboard_shortcut() { + aria.insert("keyboard_shortcut".into(), json!(v)); + } + if let Some(v) = node.access_key() { + aria.insert("access_key".into(), json!(v)); + } + if let Some(v) = node.placeholder() { + aria.insert("placeholder".into(), json!(v)); + } + if let Some(v) = node.tooltip() { + aria.insert("tooltip".into(), json!(v)); + } + if let Some(v) = node.role_description() { + aria.insert("role_description".into(), json!(v)); + } + + // Boolean / enum states. + if let Some(v) = node.is_selected() { + aria.insert("selected".into(), json!(v)); + } + if let Some(v) = node.is_expanded() { + aria.insert("expanded".into(), json!(v)); + } + if let Some(v) = node.toggled() { + aria.insert("toggled".into(), json!(format!("{v:?}"))); + } + if let Some(v) = node.orientation() { + aria.insert("orientation".into(), json!(format!("{v:?}"))); + } + + // Numeric properties. + if let Some(v) = node.numeric_value() { + aria.insert("numeric_value".into(), json!(v)); + } + if let Some(v) = node.min_numeric_value() { + aria.insert("min_numeric_value".into(), json!(v)); + } + if let Some(v) = node.max_numeric_value() { + aria.insert("max_numeric_value".into(), json!(v)); + } + if let Some(v) = node.numeric_value_step() { + aria.insert("numeric_value_step".into(), json!(v)); + } + + // Set / table properties. + if let Some(v) = node.level() { + aria.insert("level".into(), json!(v)); + } + if let Some(v) = node.position_in_set() { + aria.insert("position_in_set".into(), json!(v)); + } + if let Some(v) = node.size_of_set() { + aria.insert("size_of_set".into(), json!(v)); + } + if let Some(v) = node.row_index() { + aria.insert("row_index".into(), json!(v)); + } + if let Some(v) = node.column_index() { + aria.insert("column_index".into(), json!(v)); + } + if let Some(v) = node.row_count() { + aria.insert("row_count".into(), json!(v)); + } + if let Some(v) = node.column_count() { + aria.insert("column_count".into(), json!(v)); + } + + map.insert("aria".into(), serde_json::Value::Object(aria)); + + serde_json::Value::Object(map) +} + +/// Maps a 0-based index to a short id in the sequence `a, b, ..., z, aa, ab, +/// ...` (bijective base-26). +fn ephemeral_id(mut index: usize) -> String { + let mut bytes = Vec::new(); + loop { + bytes.push(b'a' + (index % 26) as u8); + if index < 26 { + break; + } + index = index / 26 - 1; + } + bytes.reverse(); + String::from_utf8(bytes).unwrap_or_default() +} diff --git a/vendor/gpui-pre/src/window/prompts.rs b/vendor/gpui-pre/src/window/prompts.rs new file mode 100644 index 0000000..980c6f6 --- /dev/null +++ b/vendor/gpui-pre/src/window/prompts.rs @@ -0,0 +1,232 @@ +use std::ops::Deref; + +use futures::channel::oneshot; + +use crate::{ + AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render, + StatefulInteractiveElement, Styled, div, opaque_grey, white, +}; + +use super::Window; + +/// The event emitted when a prompt's option is selected. +/// The usize is the index of the selected option, from the actions +/// passed to the prompt. +pub struct PromptResponse(pub usize); + +/// A prompt that can be rendered in the window. +pub trait Prompt: EventEmitter + Focusable {} + +impl + Focusable> Prompt for V {} + +/// A handle to a prompt that can be used to interact with it. +pub struct PromptHandle { + sender: oneshot::Sender, +} + +impl PromptHandle { + pub(crate) fn new(sender: oneshot::Sender) -> Self { + Self { sender } + } + + /// Construct a new prompt handle from a view of the appropriate types + pub fn with_view( + self, + view: Entity, + window: &mut Window, + cx: &mut App, + ) -> RenderablePromptHandle { + let mut sender = Some(self.sender); + let previous_focus = window.focused(cx); + let window_handle = window.window_handle(); + cx.subscribe(&view, move |_: Entity, e: &PromptResponse, cx| { + if let Some(sender) = sender.take() { + sender.send(e.0).ok(); + window_handle + .update(cx, |_, window, cx| { + window.prompt.take(); + if let Some(previous_focus) = &previous_focus { + window.focus(previous_focus, cx); + } + }) + .ok(); + } + }) + .detach(); + + window.focus(&view.focus_handle(cx), cx); + + RenderablePromptHandle { + view: Box::new(view), + } + } +} + +/// A prompt handle capable of being rendered in a window. +pub struct RenderablePromptHandle { + pub(crate) view: Box, +} + +/// Use this function in conjunction with [App::set_prompt_builder] to force +/// GPUI to always use the fallback prompt renderer. +pub fn fallback_prompt_renderer( + level: PromptLevel, + message: &str, + detail: Option<&str>, + actions: &[PromptButton], + handle: PromptHandle, + window: &mut Window, + cx: &mut App, +) -> RenderablePromptHandle { + let renderer = cx.new(|cx| FallbackPromptRenderer { + _level: level, + message: message.to_string(), + detail: detail.map(ToString::to_string), + actions: actions.to_vec(), + focus: cx.focus_handle(), + }); + + handle.with_view(renderer, window, cx) +} + +/// The default GPUI fallback for rendering prompts, when the platform doesn't support it. +pub struct FallbackPromptRenderer { + _level: PromptLevel, + message: String, + detail: Option, + actions: Vec, + focus: FocusHandle, +} + +impl Render for FallbackPromptRenderer { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let prompt = div() + .cursor_default() + .track_focus(&self.focus) + .w_72() + .bg(white()) + .rounded_lg() + .overflow_hidden() + .p_3() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(div().overflow_hidden().child(self.message.clone())), + ) + .children(self.detail.clone().map(|detail| { + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .text_sm() + .mb_2() + .child(div().child(detail)) + })) + .children(self.actions.iter().enumerate().map(|(ix, action)| { + div() + .flex() + .flex_row() + .justify_around() + .border_1() + .border_color(opaque_grey(0.2, 0.5)) + .mt_1() + .rounded_xs() + .cursor_pointer() + .text_sm() + .child(action.label().clone()) + .id(ix) + .on_click(cx.listener(move |_, _, _, cx| { + cx.emit(PromptResponse(ix)); + cx.stop_propagation(); + })) + })); + + div() + .size_full() + .child( + div() + .size_full() + .bg(opaque_grey(0.5, 0.6)) + .absolute() + .top_0() + .left_0(), + ) + .child( + div() + .size_full() + .absolute() + .top_0() + .left_0() + .flex() + .flex_col() + .justify_around() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(prompt), + ), + ) + } +} + +impl EventEmitter for FallbackPromptRenderer {} + +impl Focusable for FallbackPromptRenderer { + fn focus_handle(&self, _: &crate::App) -> FocusHandle { + self.focus.clone() + } +} + +pub(crate) trait PromptViewHandle { + fn any_view(&self) -> AnyView; +} + +impl PromptViewHandle for Entity { + fn any_view(&self) -> AnyView { + self.clone().into() + } +} + +pub(crate) enum PromptBuilder { + Default, + Custom( + Box< + dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle, + >, + ), +} + +impl Deref for PromptBuilder { + type Target = dyn Fn( + PromptLevel, + &str, + Option<&str>, + &[PromptButton], + PromptHandle, + &mut Window, + &mut App, + ) -> RenderablePromptHandle; + + fn deref(&self) -> &Self::Target { + match self { + Self::Default => &fallback_prompt_renderer, + Self::Custom(f) => f.as_ref(), + } + } +} diff --git a/vendor/gpui-pre/tests/action_macros.rs b/vendor/gpui-pre/tests/action_macros.rs new file mode 100644 index 0000000..66ef6fb --- /dev/null +++ b/vendor/gpui-pre/tests/action_macros.rs @@ -0,0 +1,55 @@ +use gpui::{Action, actions}; +use gpui_macros::register_action; +use schemars::JsonSchema; +use serde::Deserialize; + +#[test] +fn test_action_macros() { + actions!( + test_only, + [ + SomeAction, + /// Documented action + SomeActionWithDocs, + ] + ); + + #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] + #[action(namespace = test_only)] + #[serde(deny_unknown_fields)] + struct AnotherAction; + + #[derive(PartialEq, Clone, gpui::private::serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct RegisterableAction {} + + register_action!(RegisterableAction); + + impl gpui::Action for RegisterableAction { + fn boxed_clone(&self) -> Box { + unimplemented!() + } + + fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { + unimplemented!() + } + + fn name(&self) -> &'static str { + unimplemented!() + } + + fn name_for_type() -> &'static str + where + Self: Sized, + { + unimplemented!() + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + unimplemented!() + } + } +}