Detailed changes
@@ -108,7 +108,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
- "tiktoken-rs 0.5.4",
+ "tiktoken-rs 0.5.1",
"util",
]
@@ -119,7 +119,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3
dependencies = [
"log",
"serde",
- "toml 0.7.8",
+ "toml 0.7.6",
]
[[package]]
@@ -129,7 +129,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -148,14 +148,14 @@ dependencies = [
"mio-anonymous-pipes",
"mio-extras",
"miow 0.3.7",
- "nix 0.26.4",
+ "nix 0.26.2",
"parking_lot 0.12.1",
"regex-automata 0.1.10",
"serde",
"serde_yaml",
"signal-hook",
"signal-hook-mio",
- "toml 0.7.8",
+ "toml 0.7.6",
"unicode-width",
"vte",
"windows-sys",
@@ -218,9 +218,9 @@ dependencies = [
[[package]]
name = "anstream"
-version = "0.5.0"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
+checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -232,9 +232,9 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.3"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
[[package]]
name = "anstyle-parse"
@@ -256,9 +256,9 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
-version = "2.1.0"
+version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
+checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
dependencies = [
"anstyle",
"windows-sys",
@@ -363,7 +363,7 @@ dependencies = [
"futures-core",
"futures-io",
"once_cell",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"tokio",
]
@@ -377,7 +377,7 @@ dependencies = [
"futures-core",
"futures-io",
"memchr",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
]
[[package]]
@@ -502,13 +502,13 @@ dependencies = [
[[package]]
name = "async-recursion"
-version = "1.0.5"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
+checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -531,7 +531,7 @@ dependencies = [
"log",
"memchr",
"once_cell",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"pin-utils",
"slab",
"wasm-bindgen-futures",
@@ -545,7 +545,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
]
[[package]]
@@ -556,7 +556,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -599,7 +599,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -612,7 +612,7 @@ dependencies = [
"futures-io",
"futures-util",
"log",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"tungstenite 0.16.0",
]
@@ -701,7 +701,7 @@ dependencies = [
"axum-core",
"base64 0.13.1",
"bitflags 1.3.2",
- "bytes 1.5.0",
+ "bytes 1.4.0",
"futures-util",
"headers",
"http",
@@ -712,7 +712,7 @@ dependencies = [
"memchr",
"mime",
"percent-encoding",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"serde",
"serde_json",
"serde_urlencoded",
@@ -733,7 +733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
dependencies = [
"async-trait",
- "bytes 1.5.0",
+ "bytes 1.4.0",
"futures-util",
"http",
"http-body",
@@ -749,11 +749,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
dependencies = [
"axum",
- "bytes 1.5.0",
+ "bytes 1.4.0",
"futures-util",
"http",
"mime",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"serde",
"serde_json",
"tokio",
@@ -774,10 +774,21 @@ dependencies = [
"cfg-if 1.0.0",
"libc",
"miniz_oxide 0.7.1",
- "object 0.32.1",
+ "object 0.32.0",
"rustc-demangle",
]
+[[package]]
+name = "backtrace-on-stack-overflow"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
+dependencies = [
+ "backtrace",
+ "libc",
+ "nix 0.23.2",
+]
+
[[package]]
name = "bae"
version = "0.1.7"
@@ -799,9 +810,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
-version = "0.21.4"
+version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
+checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "base64ct"
@@ -857,7 +868,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
- "syn 2.0.37",
+ "syn 2.0.29",
"which",
]
@@ -1017,20 +1028,20 @@ dependencies = [
[[package]]
name = "bstr"
-version = "1.6.2"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
- "regex-automata 0.3.8",
+ "regex-automata 0.3.6",
"serde",
]
[[package]]
name = "bumpalo"
-version = "3.14.0"
+version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytecheck"
@@ -1059,6 +1070,20 @@ name = "bytemuck"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
[[package]]
name = "byteorder"
@@ -1078,9 +1103,9 @@ dependencies = [
[[package]]
name = "bytes"
-version = "1.5.0"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "call"
@@ -1181,6 +1206,25 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+[[package]]
+name = "cbindgen"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49"
+dependencies = [
+ "clap 3.2.25",
+ "heck 0.4.1",
+ "indexmap 1.9.3",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 1.0.109",
+ "tempfile",
+ "toml 0.5.11",
+]
+
[[package]]
name = "cc"
version = "1.0.83"
@@ -1247,7 +1291,7 @@ dependencies = [
"tempfile",
"text",
"thiserror",
- "time",
+ "time 0.3.27",
"tiny_http",
"url",
"util",
@@ -1256,17 +1300,18 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.31"
+version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
+ "time 0.1.45",
"wasm-bindgen",
- "windows-targets 0.48.5",
+ "winapi 0.3.9",
]
[[package]]
@@ -1314,9 +1359,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.4.4"
+version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
+checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
dependencies = [
"clap_builder",
"clap_derive 4.4.2",
@@ -1324,13 +1369,13 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.4.4"
+version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
+checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
dependencies = [
"anstream",
"anstyle",
- "clap_lex 0.5.1",
+ "clap_lex 0.5.0",
"strsim",
]
@@ -1356,7 +1401,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -1370,9 +1415,9 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.5.1"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "cli"
@@ -1418,7 +1463,7 @@ dependencies = [
"tempfile",
"text",
"thiserror",
- "time",
+ "time 0.3.27",
"tiny_http",
"url",
"util",
@@ -1527,7 +1572,7 @@ dependencies = [
"sqlx",
"text",
"theme",
- "time",
+ "time 0.3.27",
"tokio",
"tokio-tungstenite",
"toml 0.5.11",
@@ -1574,7 +1619,7 @@ dependencies = [
"settings",
"theme",
"theme_selector",
- "time",
+ "time 0.3.27",
"util",
"vcs_menu",
"workspace",
@@ -1606,7 +1651,7 @@ version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"memchr",
]
@@ -1775,9 +1820,9 @@ dependencies = [
[[package]]
name = "core-services"
-version = "0.2.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92567e81db522550ebaf742c5d875624ec7820c2c7ee5f8c60e4ce7c2ae3c0fd"
+checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347"
dependencies = [
"core-foundation",
]
@@ -2077,9 +2122,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.66+curl-8.3.0"
+version = "0.4.65+curl-8.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
+checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986"
dependencies = [
"cc",
"libc",
@@ -2087,14 +2132,14 @@ dependencies = [
"openssl-sys",
"pkg-config",
"vcpkg",
- "windows-sys",
+ "winapi 0.3.9",
]
[[package]]
name = "dashmap"
-version = "5.5.3"
+version = "5.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
+checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28"
dependencies = [
"cfg-if 1.0.0",
"hashbrown 0.14.0",
@@ -2168,7 +2213,7 @@ dependencies = [
"convert_case 0.4.0",
"proc-macro2",
"quote",
- "rustc_version",
+ "rustc_version 0.4.0",
"syn 1.0.109",
]
@@ -2345,9 +2390,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
-version = "1.0.14"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
+checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
[[package]]
name = "editor"
@@ -2460,9 +2505,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
-version = "0.3.31"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
+checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a"
dependencies = [
"serde",
]
@@ -2480,9 +2525,9 @@ dependencies = [
[[package]]
name = "errno"
-version = "0.3.3"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
+checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
dependencies = [
"errno-dragonfly",
"libc",
@@ -2644,12 +2689,6 @@ dependencies = [
"windows-sys",
]
-[[package]]
-name = "finl_unicode"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
-
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -2801,7 +2840,7 @@ dependencies = [
"sum_tree",
"tempfile",
"text",
- "time",
+ "time 0.3.27",
"util",
]
@@ -2939,7 +2978,7 @@ dependencies = [
"futures-io",
"memchr",
"parking",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"waker-fn",
]
@@ -2951,7 +2990,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -2980,7 +3019,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"pin-utils",
"slab",
"tokio-io",
@@ -3195,7 +3234,7 @@ dependencies = [
"sum_tree",
"taffy",
"thiserror",
- "time",
+ "time 0.3.27",
"tiny-skia",
"usvg",
"util",
@@ -3233,6 +3272,81 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "gpui3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-task",
+ "backtrace",
+ "bindgen 0.65.1",
+ "bitflags 2.4.0",
+ "block",
+ "bytemuck",
+ "cbindgen",
+ "cocoa",
+ "collections",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "ctor",
+ "derive_more",
+ "dhat",
+ "env_logger 0.9.3",
+ "etagere",
+ "font-kit",
+ "foreign-types",
+ "futures 0.3.28",
+ "gpui3_macros",
+ "gpui_macros",
+ "image",
+ "itertools 0.10.5",
+ "lazy_static",
+ "log",
+ "media",
+ "metal",
+ "num_cpus",
+ "objc",
+ "ordered-float",
+ "parking",
+ "parking_lot 0.11.2",
+ "pathfinder_geometry",
+ "plane-split",
+ "png",
+ "postage",
+ "rand 0.8.5",
+ "refineable",
+ "resvg",
+ "schemars",
+ "seahash",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "simplelog",
+ "slotmap",
+ "smallvec",
+ "smol",
+ "sqlez",
+ "sum_tree",
+ "taffy",
+ "thiserror",
+ "time 0.3.27",
+ "tiny-skia",
+ "usvg",
+ "util",
+ "uuid 1.4.1",
+ "waker-fn",
+]
+
+[[package]]
+name = "gpui3_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "gpui_macros"
version = "0.1.0"
@@ -3255,7 +3369,7 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"fnv",
"futures-core",
"futures-sink",
@@ -3264,7 +3378,7 @@ dependencies = [
"indexmap 1.9.3",
"slab",
"tokio",
- "tokio-util 0.7.9",
+ "tokio-util 0.7.8",
"tracing",
]
@@ -3316,21 +3430,22 @@ dependencies = [
[[package]]
name = "hashlink"
-version = "0.8.4"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f"
dependencies = [
"hashbrown 0.14.0",
]
[[package]]
name = "headers"
-version = "0.3.9"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
+checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
- "base64 0.21.4",
- "bytes 1.5.0",
+ "base64 0.13.1",
+ "bitflags 1.3.2",
+ "bytes 1.4.0",
"headers-core",
"http",
"httpdate",
@@ -3385,9 +3500,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
-version = "0.3.3"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hex"
@@ -3444,7 +3559,7 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"fnv",
"itoa",
]
@@ -3455,9 +3570,9 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"http",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
]
[[package]]
@@ -3480,9 +3595,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "human_bytes"
-version = "0.4.3"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e"
+checksum = "27e2b089f28ad15597b48d8c0a8fe94eeb1c1cb26ca99b6f66ac9582ae10c5e6"
[[package]]
name = "humantime"
@@ -3496,7 +3611,7 @@ version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"futures-channel",
"futures-core",
"futures-util",
@@ -3506,7 +3621,7 @@ dependencies = [
"httparse",
"httpdate",
"itoa",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"socket2 0.4.9",
"tokio",
"tower-service",
@@ -3521,7 +3636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
"hyper",
- "pin-project-lite 0.2.13",
+ "pin-project-lite 0.2.12",
"tokio",
"tokio-io-timeout",
]
@@ -3532,7 +3647,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
- "bytes 1.5.0",
+ "bytes 1.4.0",
"hyper",
"native-tls",
"tokio",
@@ -3681,7 +3796,7 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
- "hermit-abi 0.3.3",
+ "hermit-abi 0.3.2",
"libc",
"windows-sys",
]
@@ -3738,8 +3853,8 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
- "hermit-abi 0.3.3",
- "rustix 0.38.14",
+ "hermit-abi 0.3.2",
+ "rustix 0.38.8",
"windows-sys",
]
@@ -4042,9 +4157,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
-version = "0.2.148"
+version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libgit2-sys"
@@ -4136,9 +4251,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
-version = "0.4.7"
+version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lipsum"
@@ -4159,7 +4274,7 @@ dependencies = [
"async-trait",
"block",
"byteorder",
- "bytes 1.5.0",
+ "bytes 1.4.0",
"cocoa",
"collections",
"core-foundation",
@@ -4307,9 +4422,9 @@ checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
[[package]]
name = "matrixmultiply"
-version = "0.3.8"
+version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2"
+checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
dependencies = [
"autocfg",
"rawpointer",
@@ -4337,7 +4452,7 @@ dependencies = [
"anyhow",
"bindgen 0.65.1",
"block",
- "bytes 1.5.0",
+ "bytes 1.4.0",
"core-foundation",
"foreign-types",
"metal",
@@ -4346,9 +4461,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.6.3"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
+checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c"
[[package]]
name = "memfd"
@@ -4636,6 +4751,19 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "nix"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "memoffset 0.6.5",
+]
+
[[package]]
name = "nix"
version = "0.24.3"
@@ -4649,13 +4777,14 @@ dependencies = [
[[package]]
name = "nix"
-version = "0.26.4"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags 1.3.2",
"cfg-if 1.0.0",
"libc",
+ "static_assertions",
]
[[package]]
@@ -4858,7 +4987,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
- "hermit-abi 0.3.3",
+ "hermit-abi 0.3.2",
"libc",
]
@@ -4885,8 +5014,8 @@ dependencies = [
[[package]]
name = "nvim-rs"
-version = "0.6.0-pre"
-source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#0d2b1c884f3c39a76b5b7aac0b429f4624843954"
+version = "0.5.0"
+source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#d701c2790dcb2579f8f4d7003ba30e2100a7d25b"
dependencies = [
"async-trait",
"futures 0.3.28",
@@ -4895,7 +5024,7 @@ dependencies = [
"rmp",
"rmpv",
"tokio",
- "tokio-util 0.7.9",
+ "tokio-util 0.7.8",
]
[[package]]
@@ -4931,9 +5060,9 @@ dependencies = [
[[package]]
name = "object"
-version = "0.32.1"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
dependencies = [
"memchr",
]
@@ -4975,11 +5104,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
-version = "0.10.57"
+version = "0.10.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
+checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e"
dependencies = [
- "bitflags 2.4.0",
+ "bitflags 1.3.2",
"cfg-if 1.0.0",
"foreign-types",
"libc",
@@ -4996,7 +5125,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.37",
+ "syn 2.0.29",
]
[[package]]
@@ -5007,9 +5136,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
-version = "0.9.93"
+version = "0.9.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
+checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac"
dependencies = [
"cc",
"libc",
@@ -36,6 +36,8 @@ members = [
"crates/gpui_macros",
"crates/gpui2",
"crates/gpui2_macros",
+ "crates/gpui3",
+ "crates/gpui3_macros",
"crates/install_cli",
"crates/journal",
"crates/language",
@@ -65,6 +67,7 @@ members = [
"crates/sqlez_macros",
"crates/feature_flags",
"crates/storybook",
+ "crates/storybook2",
"crates/sum_tree",
"crates/terminal",
"crates/text",
@@ -3607,7 +3607,7 @@ impl<V> BorrowWindowContext for EventContext<'_, '_, '_, V> {
}
}
-pub(crate) enum Reference<'a, T> {
+pub enum Reference<'a, T> {
Immutable(&'a T),
Mutable(&'a mut T),
}
@@ -1,12 +1,12 @@
+use std::fmt::Debug;
+
use super::scene::{Path, PathVertex};
use crate::{color::Color, json::ToJson};
-use derive_more::Neg;
pub use pathfinder_geometry::*;
use rect::RectF;
use refineable::Refineable;
use serde::{Deserialize, Deserializer};
use serde_json::json;
-use std::fmt::Debug;
use vector::{vec2f, Vector2F};
pub struct PathBuilder {
@@ -167,6 +167,15 @@ pub struct Size<T: Clone + Default + Debug> {
pub height: T,
}
+impl Size<Length> {
+ pub fn full() -> Self {
+ Self {
+ width: relative(1.),
+ height: relative(1.),
+ }
+ }
+}
+
impl<S, T: Clone + Default + Debug> From<taffy::geometry::Size<S>> for Size<T>
where
S: Into<T>,
@@ -194,8 +203,8 @@ where
impl Size<DefiniteLength> {
pub fn zero() -> Self {
Self {
- width: pixels(0.).into(),
- height: pixels(0.).into(),
+ width: px(0.),
+ height: px(0.),
}
}
@@ -235,17 +244,6 @@ pub struct Edges<T: Clone + Default + Debug> {
pub left: T,
}
-impl<T: Clone + Default + Debug> Edges<T> {
- pub fn uniform(value: T) -> Self {
- Self {
- top: value.clone(),
- right: value.clone(),
- bottom: value.clone(),
- left: value.clone(),
- }
- }
-}
-
impl Edges<Length> {
pub fn auto() -> Self {
Self {
@@ -258,10 +256,10 @@ impl Edges<Length> {
pub fn zero() -> Self {
Self {
- top: pixels(0.).into(),
- right: pixels(0.).into(),
- bottom: pixels(0.).into(),
- left: pixels(0.).into(),
+ top: px(0.),
+ right: px(0.),
+ bottom: px(0.),
+ left: px(0.),
}
}
@@ -281,10 +279,10 @@ impl Edges<Length> {
impl Edges<DefiniteLength> {
pub fn zero() -> Self {
Self {
- top: pixels(0.).into(),
- right: pixels(0.).into(),
- bottom: pixels(0.).into(),
- left: pixels(0.).into(),
+ top: px(0.),
+ right: px(0.),
+ bottom: px(0.),
+ left: px(0.),
}
}
@@ -301,10 +299,10 @@ impl Edges<DefiniteLength> {
impl Edges<AbsoluteLength> {
pub fn zero() -> Self {
Self {
- top: pixels(0.),
- right: pixels(0.),
- bottom: pixels(0.),
- left: pixels(0.),
+ top: px(0.),
+ right: px(0.),
+ bottom: px(0.),
+ left: px(0.),
}
}
@@ -333,7 +331,7 @@ impl Edges<f32> {
}
}
-#[derive(Clone, Copy, Neg)]
+#[derive(Clone, Copy)]
pub enum AbsoluteLength {
Pixels(f32),
Rems(f32),
@@ -371,7 +369,7 @@ impl Default for AbsoluteLength {
}
/// A non-auto length that can be defined in pixels, rems, or percent of parent.
-#[derive(Clone, Copy, Neg)]
+#[derive(Clone, Copy)]
pub enum DefiniteLength {
Absolute(AbsoluteLength),
Relative(f32), // 0. to 1.
@@ -415,7 +413,7 @@ impl Default for DefiniteLength {
}
/// A length that can be defined in pixels, rems, percent of parent, or auto.
-#[derive(Clone, Copy, Neg)]
+#[derive(Clone, Copy)]
pub enum Length {
Definite(DefiniteLength),
Auto,
@@ -430,16 +428,20 @@ impl std::fmt::Debug for Length {
}
}
-pub fn relative(fraction: f32) -> DefiniteLength {
- DefiniteLength::Relative(fraction)
+pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
+ DefiniteLength::Relative(fraction).into()
+}
+
+pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
+ AbsoluteLength::Rems(rems).into()
}
-pub fn rems(rems: f32) -> AbsoluteLength {
- AbsoluteLength::Rems(rems)
+pub fn px<T: From<AbsoluteLength>>(pixels: f32) -> T {
+ AbsoluteLength::Pixels(pixels).into()
}
-pub fn pixels(pixels: f32) -> AbsoluteLength {
- AbsoluteLength::Pixels(pixels)
+pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
+ AbsoluteLength::Pixels(pixels).into()
}
pub fn auto() -> Length {
@@ -22,8 +22,8 @@ use std::{
};
pub struct TextLayoutCache {
- prev_frame: Mutex<HashMap<CacheKeyValue, Arc<LineLayout>>>,
- curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
+ prev_frame: Mutex<HashMap<OwnedCacheKey, Arc<LineLayout>>>,
+ curr_frame: RwLock<HashMap<OwnedCacheKey, Arc<LineLayout>>>,
fonts: Arc<dyn platform::FontSystem>,
}
@@ -56,7 +56,7 @@ impl TextLayoutCache {
font_size: f32,
runs: &'a [(usize, RunStyle)],
) -> Line {
- let key = &CacheKeyRef {
+ let key = &BorrowedCacheKey {
text,
font_size: OrderedFloat(font_size),
runs,
@@ -72,7 +72,7 @@ impl TextLayoutCache {
Line::new(layout, runs)
} else {
let layout = Arc::new(self.fonts.layout_line(text, font_size, runs));
- let key = CacheKeyValue {
+ let key = OwnedCacheKey {
text: text.into(),
font_size: OrderedFloat(font_size),
runs: SmallVec::from(runs),
@@ -84,7 +84,7 @@ impl TextLayoutCache {
}
trait CacheKey {
- fn key(&self) -> CacheKeyRef;
+ fn key(&self) -> BorrowedCacheKey;
}
impl<'a> PartialEq for (dyn CacheKey + 'a) {
@@ -102,15 +102,15 @@ impl<'a> Hash for (dyn CacheKey + 'a) {
}
#[derive(Eq)]
-struct CacheKeyValue {
+struct OwnedCacheKey {
text: String,
font_size: OrderedFloat<f32>,
runs: SmallVec<[(usize, RunStyle); 1]>,
}
-impl CacheKey for CacheKeyValue {
- fn key(&self) -> CacheKeyRef {
- CacheKeyRef {
+impl CacheKey for OwnedCacheKey {
+ fn key(&self) -> BorrowedCacheKey {
+ BorrowedCacheKey {
text: self.text.as_str(),
font_size: self.font_size,
runs: self.runs.as_slice(),
@@ -118,38 +118,38 @@ impl CacheKey for CacheKeyValue {
}
}
-impl PartialEq for CacheKeyValue {
+impl PartialEq for OwnedCacheKey {
fn eq(&self, other: &Self) -> bool {
self.key().eq(&other.key())
}
}
-impl Hash for CacheKeyValue {
+impl Hash for OwnedCacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key().hash(state);
}
}
-impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
+impl<'a> Borrow<dyn CacheKey + 'a> for OwnedCacheKey {
fn borrow(&self) -> &(dyn CacheKey + 'a) {
self as &dyn CacheKey
}
}
#[derive(Copy, Clone)]
-struct CacheKeyRef<'a> {
+struct BorrowedCacheKey<'a> {
text: &'a str,
font_size: OrderedFloat<f32>,
runs: &'a [(usize, RunStyle)],
}
-impl<'a> CacheKey for CacheKeyRef<'a> {
- fn key(&self) -> CacheKeyRef {
+impl<'a> CacheKey for BorrowedCacheKey<'a> {
+ fn key(&self) -> BorrowedCacheKey {
*self
}
}
-impl<'a> PartialEq for CacheKeyRef<'a> {
+impl<'a> PartialEq for BorrowedCacheKey<'a> {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.font_size == other.font_size
@@ -162,7 +162,7 @@ impl<'a> PartialEq for CacheKeyRef<'a> {
}
}
-impl<'a> Hash for CacheKeyRef<'a> {
+impl<'a> Hash for BorrowedCacheKey<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.text.hash(state);
self.font_size.hash(state);
@@ -12,11 +12,21 @@ use parking_lot::Mutex;
use std::sync::Arc;
use util::arc_cow::ArcCow;
-impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
+impl<V: 'static> IntoElement<V> for ArcCow<'static, str> {
type Element = Text;
fn into_element(self) -> Self::Element {
- Text { text: self.into() }
+ Text { text: self }
+ }
+}
+
+impl<V: 'static> IntoElement<V> for &'static str {
+ type Element = Text;
+
+ fn into_element(self) -> Self::Element {
+ Text {
+ text: ArcCow::from(self),
+ }
}
}
@@ -6,6 +6,7 @@ pub mod interactive;
pub mod style;
pub mod view;
pub mod view_context;
+pub mod view_handle;
pub use color::*;
pub use element::{AnyElement, Element, IntoElement, Layout, ParentElement};
@@ -22,6 +22,8 @@ use gpui2_macros::styleable_helpers;
use refineable::{Refineable, RefinementCascade};
use std::sync::Arc;
+pub type StyleCascade = RefinementCascade<Style>;
+
#[derive(Clone, Refineable, Debug)]
#[refineable(debug)]
pub struct Style {
@@ -129,7 +131,7 @@ impl Style {
color: self.text_color.map(Into::into),
font_family: self.font_family.clone(),
font_size: self.font_size.map(|size| size * cx.rem_size()),
- font_weight: self.font_weight,
+ font_weight: self.font_weight.map(Into::into),
font_style: self.font_style,
underline: None,
})
@@ -3,7 +3,10 @@ use std::{any::TypeId, rc::Rc};
use crate::{element::LayoutId, style::Style};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
-use gpui::{geometry::Size, scene::EventHandler, EventContext, Layout, MeasureParams};
+use gpui::{
+ geometry::Size, scene::EventHandler, AnyWindowHandle, BorrowWindowContext, EventContext,
+ Layout, MeasureParams, WindowContext,
+};
pub use gpui::{taffy::tree::NodeId, ViewContext as LegacyViewContext};
#[derive(Deref, DerefMut)]
@@ -77,3 +80,35 @@ impl<'a, 'b, 'c, V: 'static> ViewContext<'a, 'b, 'c, V> {
.computed_layout(layout_id)
}
}
+
+impl<'a, 'b, 'c, V: 'static> BorrowWindowContext for ViewContext<'a, 'b, 'c, V> {
+ type Result<T> = T;
+
+ fn read_window<T, F>(&self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+ where
+ F: FnOnce(&WindowContext) -> T,
+ {
+ self.legacy_cx.read_window(window, f)
+ }
+
+ fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
+ where
+ F: FnOnce(&WindowContext) -> Option<T>,
+ {
+ self.legacy_cx.read_window_optional(window, f)
+ }
+
+ fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Self::Result<T>
+ where
+ F: FnOnce(&mut WindowContext) -> T,
+ {
+ self.legacy_cx.update_window(window, f)
+ }
+
+ fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
+ where
+ F: FnOnce(&mut WindowContext) -> Option<T>,
+ {
+ self.legacy_cx.update_window_optional(window, f)
+ }
+}
@@ -0,0 +1,60 @@
+use crate::{style::Style, Element, IntoElement, ViewContext};
+use gpui::{
+ geometry::{Point, Size},
+ taffy::style::Overflow,
+ AnyElement, View, ViewHandle,
+};
+
+impl<ParentView: 'static, ChildView: View> Element<ParentView> for ViewHandle<ChildView> {
+ type PaintState = AnyElement<ChildView>;
+
+ fn layout(
+ &mut self,
+ _: &mut ParentView,
+ cx: &mut ViewContext<ParentView>,
+ ) -> anyhow::Result<(gpui::LayoutId, Self::PaintState)>
+ where
+ Self: Sized,
+ {
+ let layout_id = cx.add_layout_node(
+ Style {
+ overflow: Point {
+ x: Overflow::Hidden,
+ y: Overflow::Hidden,
+ },
+ size: Size::full(),
+ ..Default::default()
+ },
+ None,
+ )?;
+ let element = self.update(cx, |view, cx| view.render(cx));
+ Ok((layout_id, element))
+ }
+
+ fn paint(
+ &mut self,
+ _: &mut ParentView,
+ parent_origin: gpui::geometry::vector::Vector2F,
+ layout: &gpui::Layout,
+ element: &mut AnyElement<ChildView>,
+ cx: &mut ViewContext<ParentView>,
+ ) where
+ Self: Sized,
+ {
+ self.update(cx, |view, cx| {
+ let bounds = layout.bounds + parent_origin;
+ element.layout(gpui::SizeConstraint::strict(bounds.size()), view, cx);
+ cx.paint_layer(Some(layout.bounds), |cx| {
+ element.paint(bounds.origin(), bounds, view, cx);
+ });
+ })
+ }
+}
+
+impl<ParentView: 'static, ChildView: View> IntoElement<ParentView> for ViewHandle<ChildView> {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
@@ -0,0 +1,87 @@
+[package]
+name = "gpui3"
+version = "0.1.0"
+edition = "2021"
+authors = ["Nathan Sobo <nathan@zed.dev>"]
+description = "The next version of Zed's GPU-accelerated UI framework"
+publish = false
+
+[features]
+test = ["backtrace", "dhat", "env_logger", "collections/test-support"]
+
+[lib]
+path = "src/gpui3.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+gpui_macros = { path = "../gpui_macros" }
+gpui3_macros = { path = "../gpui3_macros" }
+util = { path = "../util" }
+sum_tree = { path = "../sum_tree" }
+sqlez = { path = "../sqlez" }
+async-task = "4.0.3"
+backtrace = { version = "0.3", optional = true }
+ctor.workspace = true
+derive_more.workspace = true
+dhat = { version = "0.3", optional = true }
+env_logger = { version = "0.9", optional = true }
+etagere = "0.2"
+futures.workspace = true
+image = "0.23"
+itertools = "0.10"
+lazy_static.workspace = true
+log.workspace = true
+num_cpus = "1.13"
+ordered-float.workspace = true
+parking = "2.0.0"
+parking_lot.workspace = true
+pathfinder_geometry = "0.5"
+postage.workspace = true
+rand.workspace = true
+refineable.workspace = true
+resvg = "0.14"
+seahash = "4.1"
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+thiserror.workspace = true
+time.workspace = true
+tiny-skia = "0.5"
+usvg = { version = "0.14", features = [] }
+uuid = { version = "1.1.2", features = ["v4"] }
+waker-fn = "1.1.0"
+slotmap = "1.0.6"
+bytemuck = { version = "1.14.0", features = ["derive"] }
+schemars.workspace = true
+plane-split = "0.18.0"
+bitflags = "2.4.0"
+
+[dev-dependencies]
+backtrace = "0.3"
+collections = { path = "../collections", features = ["test-support"] }
+dhat = "0.3"
+env_logger.workspace = true
+png = "0.16"
+simplelog = "0.9"
+
+[build-dependencies]
+bindgen = "0.65.1"
+cbindgen = "0.26.0"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+media = { path = "../media" }
+anyhow.workspace = true
+block = "0.1"
+cocoa = "0.24"
+core-foundation = { version = "0.9.3", features = ["with-uuid"] }
+core-graphics = "0.22.3"
+core-text = "19.2"
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" }
+foreign-types = "0.3"
+log.workspace = true
+metal = "0.21.0"
+objc = "0.2"
@@ -0,0 +1,115 @@
+use std::{
+ env,
+ path::{Path, PathBuf},
+ process::{self, Command},
+};
+
+use cbindgen::Config;
+
+fn main() {
+ generate_dispatch_bindings();
+ let header_path = generate_shader_bindings();
+ compile_metal_shaders(&header_path);
+}
+
+fn generate_dispatch_bindings() {
+ println!("cargo:rustc-link-lib=framework=System");
+ println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h");
+
+ let bindings = bindgen::Builder::default()
+ .header("src/platform/mac/dispatch.h")
+ .allowlist_var("_dispatch_main_q")
+ .allowlist_function("dispatch_async_f")
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+ .layout_tests(false)
+ .generate()
+ .expect("unable to generate bindings");
+
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("dispatch_sys.rs"))
+ .expect("couldn't write dispatch bindings");
+}
+
+fn generate_shader_bindings() -> PathBuf {
+ let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
+ let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+ let mut config = Config::default();
+ config.include_guard = Some("SCENE_H".into());
+ config.language = cbindgen::Language::C;
+ config.export.include.extend([
+ "Bounds".into(),
+ "Corners".into(),
+ "Edges".into(),
+ "Size".into(),
+ "Pixels".into(),
+ "PointF".into(),
+ "Hsla".into(),
+ "Quad".into(),
+ "QuadInputIndex".into(),
+ "QuadUniforms".into(),
+ ]);
+ config.no_includes = true;
+ config.enumeration.prefix_with_name = true;
+ cbindgen::Builder::new()
+ .with_src(crate_dir.join("src/scene.rs"))
+ .with_src(crate_dir.join("src/geometry.rs"))
+ .with_src(crate_dir.join("src/color.rs"))
+ .with_src(crate_dir.join("src/platform/mac/metal_renderer.rs"))
+ .with_config(config)
+ .generate()
+ .expect("Unable to generate bindings")
+ .write_to_file(&output_path);
+ output_path
+}
+
+fn compile_metal_shaders(header_path: &Path) {
+ let shader_path = "./src/platform/mac/shaders.metal";
+ let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
+ let metallib_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
+
+ println!("cargo:rerun-if-changed={}", header_path.display());
+ println!("cargo:rerun-if-changed={}", shader_path);
+
+ let output = Command::new("xcrun")
+ .args([
+ "-sdk",
+ "macosx",
+ "metal",
+ "-gline-tables-only",
+ "-mmacosx-version-min=10.15.7",
+ "-MO",
+ "-c",
+ shader_path,
+ "-include",
+ &header_path.to_str().unwrap(),
+ "-o",
+ ])
+ .arg(&air_output_path)
+ .output()
+ .unwrap();
+
+ if !output.status.success() {
+ eprintln!(
+ "metal shader compilation failed:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ process::exit(1);
+ }
+
+ let output = Command::new("xcrun")
+ .args(["-sdk", "macosx", "metallib"])
+ .arg(air_output_path)
+ .arg("-o")
+ .arg(metallib_output_path)
+ .output()
+ .unwrap();
+
+ if !output.status.success() {
+ eprintln!(
+ "metallib compilation failed:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ process::exit(1);
+ }
+}
@@ -0,0 +1,343 @@
+mod async_context;
+mod entity_map;
+mod model_context;
+
+pub use async_context::*;
+pub use entity_map::*;
+pub use model_context::*;
+use refineable::Refineable;
+
+use crate::{
+ current_platform, run_on_main, spawn_on_main, Context, LayoutId, MainThread, MainThreadOnly,
+ Platform, PlatformDispatcher, RootView, TextStyle, TextStyleRefinement, TextSystem, Window,
+ WindowContext, WindowHandle, WindowId,
+};
+use anyhow::{anyhow, Result};
+use collections::{HashMap, VecDeque};
+use futures::Future;
+use parking_lot::Mutex;
+use slotmap::SlotMap;
+use smallvec::SmallVec;
+use std::{
+ any::{type_name, Any, TypeId},
+ mem,
+ sync::{Arc, Weak},
+};
+use util::ResultExt;
+
+#[derive(Clone)]
+pub struct App(Arc<Mutex<MainThread<AppContext>>>);
+
+impl App {
+ pub fn production() -> Self {
+ Self::new(current_platform())
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn test() -> Self {
+ Self::new(Arc::new(super::TestPlatform::new()))
+ }
+
+ fn new(platform: Arc<dyn Platform>) -> Self {
+ let dispatcher = platform.dispatcher();
+ let text_system = Arc::new(TextSystem::new(platform.text_system()));
+ let entities = EntityMap::new();
+ let unit_entity = entities.redeem(entities.reserve(), ());
+ Self(Arc::new_cyclic(|this| {
+ Mutex::new(MainThread::new(AppContext {
+ this: this.clone(),
+ platform: MainThreadOnly::new(platform, dispatcher.clone()),
+ dispatcher,
+ text_system,
+ pending_updates: 0,
+ text_style_stack: Vec::new(),
+ state_stacks_by_type: HashMap::default(),
+ unit_entity,
+ entities,
+ windows: SlotMap::with_key(),
+ pending_effects: Default::default(),
+ observers: Default::default(),
+ layout_id_buffer: Default::default(),
+ }))
+ }))
+ }
+
+ pub fn run<F>(self, on_finish_launching: F)
+ where
+ F: 'static + FnOnce(&mut MainThread<AppContext>),
+ {
+ let this = self.clone();
+ let platform = self.0.lock().platform.clone();
+ platform.borrow_on_main_thread().run(Box::new(move || {
+ let cx = &mut *this.0.lock();
+ on_finish_launching(cx);
+ }));
+ }
+}
+
+type Handlers = SmallVec<[Arc<dyn Fn(&mut AppContext) -> bool + Send + Sync + 'static>; 2]>;
+
+pub struct AppContext {
+ this: Weak<Mutex<MainThread<AppContext>>>,
+ platform: MainThreadOnly<dyn Platform>,
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ text_system: Arc<TextSystem>,
+ pending_updates: usize,
+ pub(crate) text_style_stack: Vec<TextStyleRefinement>,
+ pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
+ pub(crate) unit_entity: Handle<()>,
+ pub(crate) entities: EntityMap,
+ pub(crate) windows: SlotMap<WindowId, Option<Window>>,
+ pub(crate) pending_effects: VecDeque<Effect>,
+ pub(crate) observers: HashMap<EntityId, Handlers>,
+ pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
+}
+
+impl AppContext {
+ fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
+ self.pending_updates += 1;
+ let result = update(self);
+ if self.pending_updates == 1 {
+ self.flush_effects();
+ }
+ self.pending_updates -= 1;
+ result
+ }
+
+ pub(crate) fn update_window<R>(
+ &mut self,
+ id: WindowId,
+ update: impl FnOnce(&mut WindowContext) -> R,
+ ) -> Result<R> {
+ self.update(|cx| {
+ let mut window = cx
+ .windows
+ .get_mut(id)
+ .ok_or_else(|| anyhow!("window not found"))?
+ .take()
+ .unwrap();
+
+ let result = update(&mut WindowContext::mutable(cx, &mut window));
+
+ cx.windows
+ .get_mut(id)
+ .ok_or_else(|| anyhow!("window not found"))?
+ .replace(window);
+
+ Ok(result)
+ })
+ }
+
+ fn flush_effects(&mut self) {
+ while let Some(effect) = self.pending_effects.pop_front() {
+ match effect {
+ Effect::Notify(entity_id) => self.apply_notify_effect(entity_id),
+ }
+ }
+
+ let dirty_window_ids = self
+ .windows
+ .iter()
+ .filter_map(|(window_id, window)| {
+ let window = window.as_ref().unwrap();
+ if window.dirty {
+ Some(window_id)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ for dirty_window_id in dirty_window_ids {
+ self.update_window(dirty_window_id, |cx| cx.draw())
+ .unwrap()
+ .log_err();
+ }
+ }
+
+ fn apply_notify_effect(&mut self, updated_entity: EntityId) {
+ if let Some(mut handlers) = self.observers.remove(&updated_entity) {
+ handlers.retain(|handler| handler(self));
+ if let Some(new_handlers) = self.observers.remove(&updated_entity) {
+ handlers.extend(new_handlers);
+ }
+ self.observers.insert(updated_entity, handlers);
+ }
+ }
+
+ pub fn to_async(&self) -> AsyncContext {
+ AsyncContext(unsafe { mem::transmute(self.this.clone()) })
+ }
+
+ pub fn run_on_main<R>(
+ &self,
+ f: impl FnOnce(&mut MainThread<AppContext>) -> R + Send + 'static,
+ ) -> impl Future<Output = R>
+ where
+ R: Send + 'static,
+ {
+ let this = self.this.upgrade().unwrap();
+ run_on_main(self.dispatcher.clone(), move || {
+ let cx = &mut *this.lock();
+ cx.update(|cx| {
+ f(unsafe { mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx) })
+ })
+ })
+ }
+
+ pub fn spawn_on_main<F, R>(
+ &self,
+ f: impl FnOnce(&mut MainThread<AppContext>) -> F + Send + 'static,
+ ) -> impl Future<Output = R>
+ where
+ F: Future<Output = R> + 'static,
+ R: Send + 'static,
+ {
+ let this = self.this.upgrade().unwrap();
+ spawn_on_main(self.dispatcher.clone(), move || {
+ let cx = &mut *this.lock();
+ cx.update(|cx| {
+ f(unsafe { mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx) })
+ })
+ })
+ }
+
+ pub fn text_system(&self) -> &Arc<TextSystem> {
+ &self.text_system
+ }
+
+ pub fn text_style(&self) -> TextStyle {
+ let mut style = TextStyle::default();
+ for refinement in &self.text_style_stack {
+ style.refine(refinement);
+ }
+ style
+ }
+
+ pub fn state<S: 'static>(&self) -> &S {
+ self.state_stacks_by_type
+ .get(&TypeId::of::<S>())
+ .and_then(|stack| stack.last())
+ .and_then(|any_state| any_state.downcast_ref::<S>())
+ .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
+ .unwrap()
+ }
+
+ pub fn state_mut<S: 'static>(&mut self) -> &mut S {
+ self.state_stacks_by_type
+ .get_mut(&TypeId::of::<S>())
+ .and_then(|stack| stack.last_mut())
+ .and_then(|any_state| any_state.downcast_mut::<S>())
+ .ok_or_else(|| anyhow!("no state of type {} exists", type_name::<S>()))
+ .unwrap()
+ }
+
+ pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
+ self.text_style_stack.push(text_style);
+ }
+
+ pub(crate) fn pop_text_style(&mut self) {
+ self.text_style_stack.pop();
+ }
+
+ pub(crate) fn push_state<T: Send + Sync + 'static>(&mut self, state: T) {
+ self.state_stacks_by_type
+ .entry(TypeId::of::<T>())
+ .or_default()
+ .push(Box::new(state));
+ }
+
+ pub(crate) fn pop_state<T: 'static>(&mut self) {
+ self.state_stacks_by_type
+ .get_mut(&TypeId::of::<T>())
+ .and_then(|stack| stack.pop())
+ .expect("state stack underflow");
+ }
+}
+
+impl Context for AppContext {
+ type EntityContext<'a, 'w, T: Send + Sync + 'static> = ModelContext<'a, T>;
+ type Result<T> = T;
+
+ fn entity<T: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+ ) -> Handle<T> {
+ let slot = self.entities.reserve();
+ let entity = build_entity(&mut ModelContext::mutable(self, slot.id));
+ self.entities.redeem(slot, entity)
+ }
+
+ fn update_entity<T: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<T>,
+ update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+ ) -> R {
+ let mut entity = self.entities.lease(handle);
+ let result = update(&mut *entity, &mut ModelContext::mutable(self, handle.id));
+ self.entities.end_lease(entity);
+ result
+ }
+}
+
+impl MainThread<AppContext> {
+ fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R {
+ self.0.update(|cx| {
+ update(unsafe {
+ std::mem::transmute::<&mut AppContext, &mut MainThread<AppContext>>(cx)
+ })
+ })
+ }
+
+ pub(crate) fn update_window<R>(
+ &mut self,
+ id: WindowId,
+ update: impl FnOnce(&mut WindowContext) -> R,
+ ) -> Result<R> {
+ self.0.update_window(id, |cx| {
+ update(unsafe {
+ std::mem::transmute::<&mut WindowContext, &mut MainThread<WindowContext>>(cx)
+ })
+ })
+ }
+
+ pub(crate) fn platform(&self) -> &dyn Platform {
+ self.platform.borrow_on_main_thread()
+ }
+
+ pub fn activate(&mut self, ignoring_other_apps: bool) {
+ self.platform().activate(ignoring_other_apps);
+ }
+
+ pub fn open_window<S: 'static + Send + Sync>(
+ &mut self,
+ options: crate::WindowOptions,
+ build_root_view: impl FnOnce(&mut WindowContext) -> RootView<S> + Send + 'static,
+ ) -> WindowHandle<S> {
+ self.update(|cx| {
+ let id = cx.windows.insert(None);
+ let handle = WindowHandle::new(id);
+ let mut window = Window::new(handle.into(), options, cx);
+ let root_view = build_root_view(&mut WindowContext::mutable(cx, &mut window));
+ window.root_view.replace(root_view.into_any());
+ cx.windows.get_mut(id).unwrap().replace(window);
+ handle
+ })
+ }
+}
+
+pub(crate) enum Effect {
+ Notify(EntityId),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::AppContext;
+
+ #[test]
+ fn test_app_context_send_sync() {
+ // This will not compile if `AppContext` does not implement `Send`
+ fn assert_send<T: Send>() {}
+ assert_send::<AppContext>();
+ }
+}
@@ -0,0 +1,52 @@
+use crate::{AnyWindowHandle, AppContext, Context, Handle, ModelContext, Result, WindowContext};
+use anyhow::anyhow;
+use parking_lot::Mutex;
+use std::sync::Weak;
+
+#[derive(Clone)]
+pub struct AsyncContext(pub(crate) Weak<Mutex<AppContext>>);
+
+impl Context for AsyncContext {
+ type EntityContext<'a, 'b, T: 'static + Send + Sync> = ModelContext<'a, T>;
+ type Result<T> = Result<T>;
+
+ fn entity<T: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+ ) -> Result<Handle<T>> {
+ let app = self
+ .0
+ .upgrade()
+ .ok_or_else(|| anyhow!("app was released"))?;
+ let mut lock = app.lock();
+ Ok(lock.entity(build_entity))
+ }
+
+ fn update_entity<T: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<T>,
+ update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+ ) -> Result<R> {
+ let app = self
+ .0
+ .upgrade()
+ .ok_or_else(|| anyhow!("app was released"))?;
+ let mut lock = app.lock();
+ Ok(lock.update_entity(handle, update))
+ }
+}
+
+impl AsyncContext {
+ pub fn update_window<T>(
+ &self,
+ handle: AnyWindowHandle,
+ update: impl FnOnce(&mut WindowContext) -> T + Send + Sync,
+ ) -> Result<T> {
+ let app = self
+ .0
+ .upgrade()
+ .ok_or_else(|| anyhow!("app was released"))?;
+ let mut app_context = app.lock();
+ app_context.update_window(handle.id, update)
+ }
+}
@@ -0,0 +1,175 @@
+use crate::Context;
+use anyhow::{anyhow, Result};
+use derive_more::{Deref, DerefMut};
+use parking_lot::{Mutex, RwLock};
+use slotmap::{SecondaryMap, SlotMap};
+use std::{
+ any::Any,
+ marker::PhantomData,
+ sync::{
+ atomic::{AtomicUsize, Ordering::SeqCst},
+ Arc, Weak,
+ },
+};
+
+slotmap::new_key_type! { pub struct EntityId; }
+
+#[derive(Deref, DerefMut)]
+pub struct Lease<T> {
+ #[deref]
+ #[deref_mut]
+ entity: Box<T>,
+ pub id: EntityId,
+}
+
+pub(crate) struct EntityMap {
+ ref_counts: Arc<RwLock<RefCounts>>,
+ entities: Arc<Mutex<SecondaryMap<EntityId, Box<dyn Any + Send + Sync>>>>,
+}
+
+impl EntityMap {
+ pub fn new() -> Self {
+ Self {
+ ref_counts: Arc::new(RwLock::new(SlotMap::with_key())),
+ entities: Arc::new(Mutex::new(SecondaryMap::new())),
+ }
+ }
+
+ pub fn reserve<T: 'static + Send + Sync>(&self) -> Slot<T> {
+ let id = self.ref_counts.write().insert(1.into());
+ Slot(Handle::new(id, Arc::downgrade(&self.ref_counts)))
+ }
+
+ pub fn redeem<T: 'static + Any + Send + Sync>(&self, slot: Slot<T>, entity: T) -> Handle<T> {
+ let handle = slot.0;
+ self.entities.lock().insert(handle.id, Box::new(entity));
+ handle
+ }
+
+ pub fn lease<T: 'static + Send + Sync>(&self, handle: &Handle<T>) -> Lease<T> {
+ let id = handle.id;
+ let entity = self
+ .entities
+ .lock()
+ .remove(id)
+ .expect("Circular entity lease. Is the entity already being updated?")
+ .downcast::<T>()
+ .unwrap();
+ Lease { id, entity }
+ }
+
+ pub fn end_lease<T: 'static + Send + Sync>(&mut self, lease: Lease<T>) {
+ self.entities.lock().insert(lease.id, lease.entity);
+ }
+
+ pub fn weak_handle<T: 'static + Send + Sync>(&self, id: EntityId) -> WeakHandle<T> {
+ WeakHandle {
+ id,
+ entity_type: PhantomData,
+ ref_counts: Arc::downgrade(&self.ref_counts),
+ }
+ }
+}
+
+#[derive(Deref, DerefMut)]
+pub struct Slot<T: Send + Sync + 'static>(Handle<T>);
+
+pub struct Handle<T: Send + Sync> {
+ pub(crate) id: EntityId,
+ entity_type: PhantomData<T>,
+ ref_counts: Weak<RwLock<RefCounts>>,
+}
+
+type RefCounts = SlotMap<EntityId, AtomicUsize>;
+
+impl<T: 'static + Send + Sync> Handle<T> {
+ pub fn new(id: EntityId, ref_counts: Weak<RwLock<RefCounts>>) -> Self {
+ Self {
+ id,
+ entity_type: PhantomData,
+ ref_counts,
+ }
+ }
+
+ pub fn downgrade(&self) -> WeakHandle<T> {
+ WeakHandle {
+ id: self.id,
+ entity_type: self.entity_type,
+ ref_counts: self.ref_counts.clone(),
+ }
+ }
+
+ /// Update the entity referenced by this handle with the given function.
+ ///
+ /// The update function receives a context appropriate for its environment.
+ /// When updating in an `AppContext`, it receives a `ModelContext`.
+ /// When updating an a `WindowContext`, it receives a `ViewContext`.
+ pub fn update<C: Context, R>(
+ &self,
+ cx: &mut C,
+ update: impl FnOnce(&mut T, &mut C::EntityContext<'_, '_, T>) -> R,
+ ) -> C::Result<R> {
+ cx.update_entity(self, update)
+ }
+}
+
+impl<T: Send + Sync> Clone for Handle<T> {
+ fn clone(&self) -> Self {
+ Self {
+ id: self.id,
+ entity_type: PhantomData,
+ ref_counts: self.ref_counts.clone(),
+ }
+ }
+}
+
+impl<T: Send + Sync> Drop for Handle<T> {
+ fn drop(&mut self) {
+ if let Some(ref_counts) = self.ref_counts.upgrade() {
+ if let Some(count) = ref_counts.read().get(self.id) {
+ let prev_count = count.fetch_sub(1, SeqCst);
+ assert_ne!(prev_count, 0, "Detected over-release of a handle.");
+ }
+ }
+ }
+}
+
+pub struct WeakHandle<T> {
+ pub(crate) id: EntityId,
+ pub(crate) entity_type: PhantomData<T>,
+ pub(crate) ref_counts: Weak<RwLock<RefCounts>>,
+}
+
+impl<T: Send + Sync + 'static> WeakHandle<T> {
+ pub fn upgrade(&self, _: &impl Context) -> Option<Handle<T>> {
+ let ref_counts = self.ref_counts.upgrade()?;
+ ref_counts.read().get(self.id).unwrap().fetch_add(1, SeqCst);
+ Some(Handle {
+ id: self.id,
+ entity_type: self.entity_type,
+ ref_counts: self.ref_counts.clone(),
+ })
+ }
+
+ /// Update 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.
+ ///
+ /// The update function receives a context appropriate for its environment.
+ /// When updating in an `AppContext`, it receives a `ModelContext`.
+ /// When updating an a `WindowContext`, it receives a `ViewContext`.
+ pub fn update<C: Context, R>(
+ &self,
+ cx: &mut C,
+ update: impl FnOnce(&mut T, &mut C::EntityContext<'_, '_, T>) -> R,
+ ) -> Result<R>
+ where
+ Result<C::Result<R>>: crate::Flatten<R>,
+ {
+ crate::Flatten::flatten(
+ self.upgrade(cx)
+ .ok_or_else(|| anyhow!("entity release"))
+ .map(|this| cx.update_entity(&this, update)),
+ )
+ }
+}
@@ -0,0 +1,87 @@
+use crate::{AppContext, Context, Effect, EntityId, Handle, Reference, WeakHandle};
+use std::{marker::PhantomData, sync::Arc};
+
+pub struct ModelContext<'a, T> {
+ app: Reference<'a, AppContext>,
+ entity_type: PhantomData<T>,
+ entity_id: EntityId,
+}
+
+impl<'a, T: Send + Sync + 'static> ModelContext<'a, T> {
+ pub(crate) fn mutable(app: &'a mut AppContext, entity_id: EntityId) -> Self {
+ Self {
+ app: Reference::Mutable(app),
+ entity_type: PhantomData,
+ entity_id,
+ }
+ }
+
+ // todo!
+ // fn update<R>(&mut self, update: impl FnOnce(&mut T, &mut Self) -> R) -> R {
+ // let mut entity = self
+ // .app
+ // .entities
+ // .get_mut(self.entity_id)
+ // .unwrap()
+ // .take()
+ // .unwrap();
+ // let result = update(entity.downcast_mut::<T>().unwrap(), self);
+ // self.app
+ // .entities
+ // .get_mut(self.entity_id)
+ // .unwrap()
+ // .replace(entity);
+ // result
+ // }
+
+ pub fn handle(&self) -> WeakHandle<T> {
+ self.app.entities.weak_handle(self.entity_id)
+ }
+
+ pub fn observe<E: Send + Sync + 'static>(
+ &mut self,
+ handle: &Handle<E>,
+ on_notify: impl Fn(&mut T, Handle<E>, &mut ModelContext<'_, T>) + Send + Sync + 'static,
+ ) {
+ let this = self.handle();
+ let handle = handle.downgrade();
+ self.app
+ .observers
+ .entry(handle.id)
+ .or_default()
+ .push(Arc::new(move |cx| {
+ if let Some((this, handle)) = this.upgrade(cx).zip(handle.upgrade(cx)) {
+ this.update(cx, |this, cx| on_notify(this, handle, cx));
+ true
+ } else {
+ false
+ }
+ }));
+ }
+
+ pub fn notify(&mut self) {
+ self.app
+ .pending_effects
+ .push_back(Effect::Notify(self.entity_id));
+ }
+}
+
+impl<'a, T: 'static> Context for ModelContext<'a, T> {
+ type EntityContext<'b, 'c, U: Send + Sync + 'static> = ModelContext<'b, U>;
+ type Result<U> = U;
+
+ fn entity<U: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, U>) -> U,
+ ) -> Handle<U> {
+ self.app.entity(build_entity)
+ }
+
+ fn update_entity<U: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<U>,
+ update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R,
+ ) -> R {
+ self.app.update_entity(handle, update)
+ }
+}
@@ -0,0 +1,238 @@
+#![allow(dead_code)]
+
+use bytemuck::{Pod, Zeroable};
+use serde::de::{self, Deserialize, Deserializer, Visitor};
+use std::fmt;
+use std::num::ParseIntError;
+
+pub fn rgb(hex: u32) -> Rgba {
+ let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
+ let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
+ let b = (hex & 0xFF) as f32 / 255.0;
+ Rgba { r, g, b, a: 1.0 }.into()
+}
+
+#[derive(Clone, Copy, Default, Debug)]
+pub struct Rgba {
+ pub r: f32,
+ pub g: f32,
+ pub b: f32,
+ pub a: f32,
+}
+
+impl Rgba {
+ pub fn blend(&self, other: Rgba) -> Self {
+ if other.a >= 1.0 {
+ return other;
+ } else if other.a <= 0.0 {
+ return *self;
+ } else {
+ return 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,
+ };
+ }
+ }
+}
+
+struct RgbaVisitor;
+
+impl<'de> Visitor<'de> 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<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
+ if value.len() == 7 || value.len() == 9 {
+ let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0;
+ let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0;
+ let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0;
+ let a = if value.len() == 9 {
+ u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0
+ } else {
+ 1.0
+ };
+ Ok(Rgba { r, g, b, a })
+ } else {
+ Err(E::custom(
+ "Bad format for RGBA. Expected #rrggbb or #rrggbbaa.",
+ ))
+ }
+ }
+}
+
+impl<'de> Deserialize<'de> for Rgba {
+ fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ deserializer.deserialize_str(RgbaVisitor)
+ }
+}
+
+impl From<Hsla> 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,
+ g,
+ b,
+ a: color.a,
+ }
+ }
+}
+
+impl TryFrom<&'_ str> for Rgba {
+ type Error = ParseIntError;
+
+ fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
+ let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
+ let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
+ let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
+ let a = if value.len() > 7 {
+ u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
+ } else {
+ 1.0
+ };
+
+ Ok(Rgba { r, g, b, a })
+ }
+}
+
+#[derive(Default, Copy, Clone, Debug, PartialEq, Zeroable, Pod)]
+#[repr(C)]
+pub struct Hsla {
+ pub h: f32,
+ pub s: f32,
+ pub l: f32,
+ pub a: f32,
+}
+
+impl Eq for Hsla {}
+
+pub 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.),
+ }
+}
+
+pub fn black() -> Hsla {
+ Hsla {
+ h: 0.,
+ s: 0.,
+ l: 0.,
+ a: 1.,
+ }
+}
+
+impl Hsla {
+ /// Returns true if the HSLA color is fully transparent, false otherwise.
+ pub fn is_transparent(&self) -> bool {
+ self.a == 0.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 it's 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 {
+ return other;
+ } else if alpha <= 0.0 {
+ return self;
+ } else {
+ let converted_self = Rgba::from(self);
+ let converted_other = Rgba::from(other);
+ let blended_rgb = converted_self.blend(converted_other);
+ return Hsla::from(blended_rgb);
+ }
+ }
+
+ /// 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.);
+ }
+}
+
+impl From<Rgba> 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<'de> Deserialize<'de> for Hsla {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ // First, deserialize it into Rgba
+ let rgba = Rgba::deserialize(deserializer)?;
+
+ // Then, use the From<Rgba> for Hsla implementation to convert it
+ Ok(Hsla::from(rgba))
+ }
+}
@@ -0,0 +1,166 @@
+use super::{Layout, LayoutId, Pixels, Point, Result, ViewContext};
+pub(crate) use smallvec::SmallVec;
+
+pub trait Element: 'static {
+ type State;
+ type FrameState;
+
+ fn layout(
+ &mut self,
+ state: &mut Self::State,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<(LayoutId, Self::FrameState)>;
+
+ fn paint(
+ &mut self,
+ layout: Layout,
+ state: &mut Self::State,
+ frame_state: &mut Self::FrameState,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<()>;
+}
+
+pub trait ParentElement<S> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<S>; 2]>;
+
+ fn child(mut self, child: impl IntoAnyElement<S>) -> Self
+ where
+ Self: Sized,
+ {
+ self.children_mut().push(child.into_any());
+ self
+ }
+
+ fn children(mut self, iter: impl IntoIterator<Item = impl IntoAnyElement<S>>) -> Self
+ where
+ Self: Sized,
+ {
+ self.children_mut()
+ .extend(iter.into_iter().map(|item| item.into_any()));
+ self
+ }
+}
+
+trait ElementObject<S> {
+ fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> Result<LayoutId>;
+ fn paint(
+ &mut self,
+ state: &mut S,
+ offset: Option<Point<Pixels>>,
+ cx: &mut ViewContext<S>,
+ ) -> Result<()>;
+}
+
+struct RenderedElement<E: Element> {
+ element: E,
+ phase: ElementRenderPhase<E::FrameState>,
+}
+
+#[derive(Default)]
+enum ElementRenderPhase<S> {
+ #[default]
+ Rendered,
+ LayoutRequested {
+ layout_id: LayoutId,
+ frame_state: S,
+ },
+ Painted {
+ layout: Layout,
+ frame_state: S,
+ },
+}
+
+/// Internal struct that wraps an element to store Layout and FrameState after the element is rendered.
+/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
+/// improved usability.
+impl<E: Element> RenderedElement<E> {
+ fn new(element: E) -> Self {
+ RenderedElement {
+ element,
+ phase: ElementRenderPhase::Rendered,
+ }
+ }
+}
+
+impl<E: Element> ElementObject<E::State> for RenderedElement<E> {
+ fn layout(&mut self, state: &mut E::State, cx: &mut ViewContext<E::State>) -> Result<LayoutId> {
+ let (layout_id, frame_state) = self.element.layout(state, cx)?;
+ self.phase = ElementRenderPhase::LayoutRequested {
+ layout_id,
+ frame_state,
+ };
+ Ok(layout_id)
+ }
+
+ fn paint(
+ &mut self,
+ state: &mut E::State,
+ offset: Option<Point<Pixels>>,
+ cx: &mut ViewContext<E::State>,
+ ) -> Result<()> {
+ self.phase = match std::mem::take(&mut self.phase) {
+ ElementRenderPhase::Rendered => panic!("must call layout before paint"),
+
+ ElementRenderPhase::LayoutRequested {
+ layout_id,
+ mut frame_state,
+ } => {
+ let mut layout = cx.layout(layout_id)?.clone();
+ offset.map(|offset| layout.bounds.origin += offset);
+ self.element
+ .paint(layout.clone(), state, &mut frame_state, cx)?;
+ ElementRenderPhase::Painted {
+ layout,
+ frame_state,
+ }
+ }
+
+ ElementRenderPhase::Painted {
+ layout,
+ mut frame_state,
+ } => {
+ self.element
+ .paint(layout.clone(), state, &mut frame_state, cx)?;
+ ElementRenderPhase::Painted {
+ layout,
+ frame_state,
+ }
+ }
+ };
+
+ Ok(())
+ }
+}
+
+pub struct AnyElement<S>(Box<dyn ElementObject<S>>);
+
+impl<S> AnyElement<S> {
+ pub fn layout(&mut self, state: &mut S, cx: &mut ViewContext<S>) -> Result<LayoutId> {
+ self.0.layout(state, cx)
+ }
+
+ pub fn paint(
+ &mut self,
+ state: &mut S,
+ offset: Option<Point<Pixels>>,
+ cx: &mut ViewContext<S>,
+ ) -> Result<()> {
+ self.0.paint(state, offset, cx)
+ }
+}
+
+pub trait IntoAnyElement<S> {
+ fn into_any(self) -> AnyElement<S>;
+}
+
+impl<E: Element> IntoAnyElement<E::State> for E {
+ fn into_any(self) -> AnyElement<E::State> {
+ AnyElement(Box::new(RenderedElement::new(self)))
+ }
+}
+
+impl<S> IntoAnyElement<S> for AnyElement<S> {
+ fn into_any(self) -> AnyElement<S> {
+ self
+ }
+}
@@ -0,0 +1,11 @@
+mod div;
+mod img;
+mod stateless;
+mod svg;
+mod text;
+
+pub use div::*;
+pub use img::*;
+pub use stateless::*;
+pub use svg::*;
+pub use text::*;
@@ -0,0 +1,297 @@
+use crate::{
+ AnyElement, Bounds, Element, Layout, LayoutId, Overflow, ParentElement, Pixels, Point,
+ Refineable, RefinementCascade, Result, StackContext, Style, StyleHelpers, Styled, ViewContext,
+};
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+use std::sync::Arc;
+use util::ResultExt;
+
+pub struct Div<S: 'static> {
+ styles: RefinementCascade<Style>,
+ // handlers: InteractionHandlers<V>,
+ children: SmallVec<[AnyElement<S>; 2]>,
+ scroll_state: Option<ScrollState>,
+}
+
+pub fn div<S>() -> Div<S> {
+ Div {
+ styles: Default::default(),
+ // handlers: Default::default(),
+ children: Default::default(),
+ scroll_state: None,
+ }
+}
+
+impl<S: 'static + Send + Sync> Element for Div<S> {
+ type State = S;
+ type FrameState = Vec<LayoutId>;
+
+ fn layout(
+ &mut self,
+ view: &mut S,
+ cx: &mut ViewContext<S>,
+ ) -> Result<(LayoutId, Self::FrameState)> {
+ let style = self.computed_style();
+ let child_layout_ids = if let Some(text_style) = style.text_style(cx) {
+ cx.with_text_style(text_style.clone(), |cx| self.layout_children(view, cx))?
+ } else {
+ self.layout_children(view, cx)?
+ };
+
+ Ok((
+ cx.request_layout(style.into(), child_layout_ids.clone())?,
+ child_layout_ids,
+ ))
+ }
+
+ fn paint(
+ &mut self,
+ layout: Layout,
+ state: &mut S,
+ child_layouts: &mut Self::FrameState,
+ cx: &mut ViewContext<S>,
+ ) -> Result<()> {
+ let Layout { order, bounds } = layout;
+
+ let style = self.computed_style();
+ style.paint(order, bounds, cx);
+ let overflow = &style.overflow;
+ // // todo!("support only one dimension being hidden")
+ // if style.overflow.y != Overflow::Visible || style.overflow.x != Overflow::Visible {
+ // cx.scene().push_layer(Some(bounds));
+ // pop_layer = true;
+ // }
+ if let Some(text_style) = style.text_style(cx) {
+ cx.with_text_style(text_style.clone(), |cx| {
+ self.paint_children(overflow, state, cx)
+ })?;
+ } else {
+ self.paint_children(overflow, state, cx)?;
+ }
+
+ self.handle_scroll(order, bounds, style.overflow.clone(), child_layouts, cx);
+
+ // todo!("enable inspector")
+ // if cx.is_inspector_enabled() {
+ // self.paint_inspector(parent_origin, layout, cx);
+ // }
+ //
+ Ok(())
+ }
+}
+
+impl<S: 'static> Div<S> {
+ pub fn overflow_hidden(mut self) -> Self {
+ self.declared_style().overflow.x = Some(Overflow::Hidden);
+ self.declared_style().overflow.y = Some(Overflow::Hidden);
+ self
+ }
+
+ pub fn overflow_hidden_x(mut self) -> Self {
+ self.declared_style().overflow.x = Some(Overflow::Hidden);
+ self
+ }
+
+ pub fn overflow_hidden_y(mut self) -> Self {
+ self.declared_style().overflow.y = Some(Overflow::Hidden);
+ self
+ }
+
+ pub fn overflow_scroll(mut self, scroll_state: ScrollState) -> Self {
+ self.scroll_state = Some(scroll_state);
+ self.declared_style().overflow.x = Some(Overflow::Scroll);
+ self.declared_style().overflow.y = Some(Overflow::Scroll);
+ self
+ }
+
+ pub fn overflow_x_scroll(mut self, scroll_state: ScrollState) -> Self {
+ self.scroll_state = Some(scroll_state);
+ self.declared_style().overflow.x = Some(Overflow::Scroll);
+ self
+ }
+
+ pub fn overflow_y_scroll(mut self, scroll_state: ScrollState) -> Self {
+ self.scroll_state = Some(scroll_state);
+ self.declared_style().overflow.y = Some(Overflow::Scroll);
+ self
+ }
+
+ fn scroll_offset(&self, overflow: &Point<Overflow>) -> Point<Pixels> {
+ let mut offset = Point::default();
+ if overflow.y == Overflow::Scroll {
+ offset.y = self.scroll_state.as_ref().unwrap().y();
+ }
+ if overflow.x == Overflow::Scroll {
+ offset.x = self.scroll_state.as_ref().unwrap().x();
+ }
+
+ offset
+ }
+
+ fn layout_children(&mut self, view: &mut S, cx: &mut ViewContext<S>) -> Result<Vec<LayoutId>> {
+ self.children
+ .iter_mut()
+ .map(|child| child.layout(view, cx))
+ .collect::<Result<Vec<LayoutId>>>()
+ }
+
+ fn paint_children(
+ &mut self,
+ overflow: &Point<Overflow>,
+ state: &mut S,
+ cx: &mut ViewContext<S>,
+ ) -> Result<()> {
+ let scroll_offset = self.scroll_offset(overflow);
+ for child in &mut self.children {
+ child.paint(state, Some(scroll_offset), cx)?;
+ }
+ Ok(())
+ }
+
+ fn handle_scroll(
+ &mut self,
+ _order: u32,
+ bounds: Bounds<Pixels>,
+ overflow: Point<Overflow>,
+ child_layout_ids: &[LayoutId],
+ cx: &mut ViewContext<S>,
+ ) {
+ if overflow.y == Overflow::Scroll || overflow.x == Overflow::Scroll {
+ let mut scroll_max = Point::default();
+ for child_layout_id in child_layout_ids {
+ if let Some(child_layout) = cx.layout(*child_layout_id).log_err() {
+ scroll_max = scroll_max.max(&child_layout.bounds.lower_right());
+ }
+ }
+ scroll_max -= bounds.size;
+
+ // todo!("handle scroll")
+ // let scroll_state = self.scroll_state.as_ref().unwrap().clone();
+ // cx.on_event(order, move |_, event: &ScrollWheelEvent, cx| {
+ // if bounds.contains_point(event.position) {
+ // let scroll_delta = match event.delta {
+ // ScrollDelta::Pixels(delta) => delta,
+ // ScrollDelta::Lines(delta) => cx.text_style().font_size * delta,
+ // };
+ // if overflow.x == Overflow::Scroll {
+ // scroll_state.set_x(
+ // (scroll_state.x() - scroll_delta.x())
+ // .max(px(0.))
+ // .min(scroll_max.x),
+ // );
+ // }
+ // if overflow.y == Overflow::Scroll {
+ // scroll_state.set_y(
+ // (scroll_state.y() - scroll_delta.y())
+ // .max(px(0.))
+ // .min(scroll_max.y),
+ // );
+ // }
+ // cx.repaint();
+ // } else {
+ // cx.bubble_event();
+ // }
+ // })
+ }
+ }
+
+ // fn paint_inspector(
+ // &self,
+ // parent_origin: Point<Pixels>,
+ // layout: &Layout,
+ // cx: &mut ViewContext<V>,
+ // ) {
+ // let style = self.styles.merged();
+ // let bounds = layout.bounds;
+
+ // let hovered = bounds.contains_point(cx.mouse_position());
+ // if hovered {
+ // let rem_size = cx.rem_size();
+ // // cx.scene().push_quad(scene::Quad {
+ // // bounds,
+ // // background: Some(hsla(0., 0., 1., 0.05).into()),
+ // // border: gpui::Border {
+ // // color: hsla(0., 0., 1., 0.2).into(),
+ // // top: 1.,
+ // // right: 1.,
+ // // bottom: 1.,
+ // // left: 1.,
+ // // },
+ // // corner_radii: CornerRadii::default()
+ // // .refined(&style.corner_radii)
+ // // .to_gpui(bounds.size(), rem_size),
+ // // })
+ // }
+
+ // // let pressed = Cell::new(hovered && cx.is_mouse_down(MouseButton::Left));
+ // // cx.on_event(layout.order, move |_, event: &MouseButtonEvent, _| {
+ // // if bounds.contains_point(event.position) {
+ // // if event.is_down {
+ // // pressed.set(true);
+ // // } else if pressed.get() {
+ // // pressed.set(false);
+ // // eprintln!("clicked div {:?} {:#?}", bounds, style);
+ // // }
+ // // }
+ // // });
+
+ // // let hovered = Cell::new(hovered);
+ // // cx.on_event(layout.order, move |_, event: &MouseMovedEvent, cx| {
+ // // cx.bubble_event();
+ // // let hovered_now = bounds.contains_point(event.position);
+ // // if hovered.get() != hovered_now {
+ // // hovered.set(hovered_now);
+ // // cx.repaint();
+ // // }
+ // // });
+ // }
+ //
+}
+
+impl<V> Styled for Div<V> {
+ type Style = Style;
+
+ fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+ &mut self.styles
+ }
+
+ fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+ self.styles.base()
+ }
+}
+
+impl<V> StyleHelpers for Div<V> {}
+
+// impl<V> Interactive<V> for Div<V> {
+// fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+// &mut self.handlers
+// }
+// }
+
+impl<V: 'static> ParentElement<V> for Div<V> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+ &mut self.children
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct ScrollState(Arc<Mutex<Point<Pixels>>>);
+
+impl ScrollState {
+ pub fn x(&self) -> Pixels {
+ self.0.lock().x
+ }
+
+ pub fn set_x(&self, value: Pixels) {
+ self.0.lock().x = value;
+ }
+
+ pub fn y(&self) -> Pixels {
+ self.0.lock().y
+ }
+
+ pub fn set_y(&self, value: Pixels) {
+ self.0.lock().y = value;
+ }
+}
@@ -0,0 +1,105 @@
+use crate::{
+ element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+ interactive::{InteractionHandlers, Interactive},
+ style::{Style, StyleHelpers, Styleable},
+ ViewContext,
+};
+use anyhow::Result;
+use gpui::{geometry::vector::Vector2F, platform::MouseMovedEvent, LayoutId};
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use smallvec::SmallVec;
+use std::{cell::Cell, rc::Rc};
+
+pub struct Hoverable<E: Styleable> {
+ hovered: Rc<Cell<bool>>,
+ cascade_slot: CascadeSlot,
+ hovered_style: <E::Style as Refineable>::Refinement,
+ child: E,
+}
+
+pub fn hoverable<E: Styleable>(mut child: E) -> Hoverable<E> {
+ Hoverable {
+ hovered: Rc::new(Cell::new(false)),
+ cascade_slot: child.style_cascade().reserve(),
+ hovered_style: Default::default(),
+ child,
+ }
+}
+
+impl<E: Styleable> Styleable for Hoverable<E> {
+ type Style = E::Style;
+
+ fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+ self.child.style_cascade()
+ }
+
+ fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+ &mut self.hovered_style
+ }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Hoverable<E> {
+ type PaintState = E::PaintState;
+
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut ViewContext<V>,
+ ) -> Result<(LayoutId, Self::PaintState)>
+ where
+ Self: Sized,
+ {
+ Ok(self.child.layout(view, cx)?)
+ }
+
+ fn paint(
+ &mut self,
+ view: &mut V,
+ parent_origin: Vector2F,
+ layout: &Layout,
+ paint_state: &mut Self::PaintState,
+ cx: &mut ViewContext<V>,
+ ) where
+ Self: Sized,
+ {
+ let bounds = layout.bounds + parent_origin;
+ self.hovered.set(bounds.contains_point(cx.mouse_position()));
+
+ let slot = self.cascade_slot;
+ let style = self.hovered.get().then_some(self.hovered_style.clone());
+ self.style_cascade().set(slot, style);
+
+ let hovered = self.hovered.clone();
+ cx.on_event(layout.order, move |_view, _: &MouseMovedEvent, cx| {
+ cx.bubble_event();
+ if bounds.contains_point(cx.mouse_position()) != hovered.get() {
+ cx.repaint();
+ }
+ });
+
+ self.child
+ .paint(view, parent_origin, layout, paint_state, cx);
+ }
+}
+
+impl<E: Styleable<Style = Style>> StyleHelpers for Hoverable<E> {}
+
+impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Hoverable<E> {
+ fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+ self.child.interaction_handlers()
+ }
+}
+
+impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Hoverable<E> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+ self.child.children_mut()
+ }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Hoverable<E> {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
@@ -0,0 +1,103 @@
+use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled};
+use refineable::RefinementCascade;
+use std::marker::PhantomData;
+use util::arc_cow::ArcCow;
+
+pub struct Img<S> {
+ style: RefinementCascade<Style>,
+ uri: Option<ArcCow<'static, str>>,
+ state_type: PhantomData<S>,
+}
+
+pub fn img<S>() -> Img<S> {
+ Img {
+ style: RefinementCascade::default(),
+ uri: None,
+ state_type: PhantomData,
+ }
+}
+
+impl<S> Img<S> {
+ pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self {
+ self.uri = Some(uri.into());
+ self
+ }
+}
+
+impl<S: 'static> Element for Img<S> {
+ type State = S;
+ type FrameState = ();
+
+ fn layout(
+ &mut self,
+ _: &mut Self::State,
+ cx: &mut crate::ViewContext<Self::State>,
+ ) -> anyhow::Result<(LayoutId, Self::FrameState)>
+ where
+ Self: Sized,
+ {
+ let style = self.computed_style();
+ let layout_id = cx.request_layout(style, [])?;
+ Ok((layout_id, ()))
+ }
+
+ fn paint(
+ &mut self,
+ layout: Layout,
+ _: &mut Self::State,
+ _: &mut Self::FrameState,
+ cx: &mut crate::ViewContext<Self::State>,
+ ) -> Result<()> {
+ let style = self.computed_style();
+ let order = layout.order;
+ let bounds = layout.bounds;
+
+ style.paint(order, bounds, cx);
+
+ // if let Some(uri) = &self.uri {
+ // let image_future = cx.image_cache.get(uri.clone());
+ // if let Some(data) = image_future
+ // .clone()
+ // .now_or_never()
+ // .and_then(ResultExt::log_err)
+ // {
+ // let rem_size = cx.rem_size();
+ // cx.scene().push_image(scene::Image {
+ // bounds,
+ // border: gpui::Border {
+ // color: style.border_color.unwrap_or_default().into(),
+ // top: style.border_widths.top.to_pixels(rem_size),
+ // right: style.border_widths.right.to_pixels(rem_size),
+ // bottom: style.border_widths.bottom.to_pixels(rem_size),
+ // left: style.border_widths.left.to_pixels(rem_size),
+ // },
+ // corner_radii: style.corner_radii.to_gpui(bounds.size(), rem_size),
+ // grayscale: false,
+ // data,
+ // })
+ // } else {
+ // cx.spawn(|this, mut cx| async move {
+ // if image_future.await.log_err().is_some() {
+ // this.update(&mut cx, |_, cx| cx.notify()).ok();
+ // }
+ // })
+ // .detach();
+ // }
+ // }
+ Ok(())
+ }
+}
+
+impl<S> Styled for Img<S> {
+ type Style = Style;
+
+ fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+ &mut self.style
+ }
+
+ fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
+ self.style.base()
+ }
+}
+
+impl<S> StyleHelpers for Img<S> {}
@@ -0,0 +1,108 @@
+use crate::{
+ element::{AnyElement, Element, IntoElement, Layout, ParentElement},
+ interactive::{InteractionHandlers, Interactive},
+ style::{Style, StyleHelpers, Styleable},
+ ViewContext,
+};
+use anyhow::Result;
+use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
+use refineable::{CascadeSlot, Refineable, RefinementCascade};
+use smallvec::SmallVec;
+use std::{cell::Cell, rc::Rc};
+
+pub struct Pressable<E: Styleable> {
+ pressed: Rc<Cell<bool>>,
+ pressed_style: <E::Style as Refineable>::Refinement,
+ cascade_slot: CascadeSlot,
+ child: E,
+}
+
+pub fn pressable<E: Styleable>(mut child: E) -> Pressable<E> {
+ Pressable {
+ pressed: Rc::new(Cell::new(false)),
+ pressed_style: Default::default(),
+ cascade_slot: child.style_cascade().reserve(),
+ child,
+ }
+}
+
+impl<E: Styleable> Styleable for Pressable<E> {
+ type Style = E::Style;
+
+ fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement {
+ &mut self.pressed_style
+ }
+
+ fn style_cascade(&mut self) -> &mut RefinementCascade<E::Style> {
+ self.child.style_cascade()
+ }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
+ type PaintState = E::PaintState;
+
+ fn layout(
+ &mut self,
+ view: &mut V,
+ cx: &mut ViewContext<V>,
+ ) -> Result<(LayoutId, Self::PaintState)>
+ where
+ Self: Sized,
+ {
+ self.child.layout(view, cx)
+ }
+
+ fn paint(
+ &mut self,
+ view: &mut V,
+ parent_origin: Vector2F,
+ layout: &Layout,
+ paint_state: &mut Self::PaintState,
+ cx: &mut ViewContext<V>,
+ ) where
+ Self: Sized,
+ {
+ let slot = self.cascade_slot;
+ let style = self.pressed.get().then_some(self.pressed_style.clone());
+ self.style_cascade().set(slot, style);
+
+ let pressed = self.pressed.clone();
+ let bounds = layout.bounds + parent_origin;
+ cx.on_event(layout.order, move |_view, event: &MouseButtonEvent, cx| {
+ if event.is_down {
+ if bounds.contains_point(event.position) {
+ pressed.set(true);
+ cx.repaint();
+ }
+ } else if pressed.get() {
+ pressed.set(false);
+ cx.repaint();
+ }
+ });
+
+ self.child
+ .paint(view, parent_origin, layout, paint_state, cx);
+ }
+}
+
+impl<E: Styleable<Style = Style>> StyleHelpers for Pressable<E> {}
+
+impl<V: 'static, E: Interactive<V> + Styleable> Interactive<V> for Pressable<E> {
+ fn interaction_handlers(&mut self) -> &mut InteractionHandlers<V> {
+ self.child.interaction_handlers()
+ }
+}
+
+impl<V: 'static, E: ParentElement<V> + Styleable> ParentElement<V> for Pressable<E> {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+ self.child.children_mut()
+ }
+}
+
+impl<V: 'static, E: Element<V> + Styleable> IntoElement<V> for Pressable<E> {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
@@ -0,0 +1,30 @@
+use crate::Element;
+use std::marker::PhantomData;
+
+pub struct Stateless<E: Element<State = ()>, S> {
+ element: E,
+ parent_state_type: PhantomData<S>,
+}
+
+impl<E: Element<State = ()>, S: Send + Sync + 'static> Element for Stateless<E, S> {
+ type State = S;
+ type FrameState = E::FrameState;
+
+ fn layout(
+ &mut self,
+ _: &mut Self::State,
+ cx: &mut crate::ViewContext<Self::State>,
+ ) -> anyhow::Result<(crate::LayoutId, Self::FrameState)> {
+ cx.erase_state(|cx| self.element.layout(&mut (), cx))
+ }
+
+ fn paint(
+ &mut self,
+ layout: crate::Layout,
+ _: &mut Self::State,
+ frame_state: &mut Self::FrameState,
+ cx: &mut crate::ViewContext<Self::State>,
+ ) -> anyhow::Result<()> {
+ cx.erase_state(|cx| self.element.paint(layout, &mut (), frame_state, cx))
+ }
+}
@@ -0,0 +1,82 @@
+use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled};
+use refineable::RefinementCascade;
+use std::{borrow::Cow, marker::PhantomData};
+
+pub struct Svg<S> {
+ path: Option<Cow<'static, str>>,
+ style: RefinementCascade<Style>,
+ state_type: PhantomData<S>,
+}
+
+pub fn svg<S>() -> Svg<S> {
+ Svg {
+ path: None,
+ style: RefinementCascade::<Style>::default(),
+ state_type: PhantomData,
+ }
+}
+
+impl<S> Svg<S> {
+ pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
+ self.path = Some(path.into());
+ self
+ }
+}
+
+impl<S: 'static> Element for Svg<S> {
+ type State = S;
+ type FrameState = ();
+
+ fn layout(
+ &mut self,
+ _: &mut S,
+ cx: &mut crate::ViewContext<S>,
+ ) -> anyhow::Result<(LayoutId, Self::FrameState)>
+ where
+ Self: Sized,
+ {
+ let style = self.computed_style();
+ Ok((cx.request_layout(style, [])?, ()))
+ }
+
+ fn paint(
+ &mut self,
+ _layout: Layout,
+ _: &mut Self::State,
+ _: &mut Self::FrameState,
+ _cx: &mut crate::ViewContext<S>,
+ ) -> Result<()>
+ where
+ Self: Sized,
+ {
+ // todo!
+ // let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
+ // if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
+ // if let Some(svg_tree) = cx.asset_cache.svg(path).log_err() {
+ // let icon = scene::Icon {
+ // bounds: layout.bounds + parent_origin,
+ // svg: svg_tree,
+ // path: path.clone(),
+ // color: Rgba::from(fill_color).into(),
+ // };
+
+ // cx.scene().push_icon(icon);
+ // }
+ // }
+ Ok(())
+ }
+}
+
+impl<S> Styled for Svg<S> {
+ type Style = Style;
+
+ fn style_cascade(&mut self) -> &mut refineable::RefinementCascade<Self::Style> {
+ &mut self.style
+ }
+
+ fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
+ self.style.base()
+ }
+}
+
+impl<S> StyleHelpers for Svg<S> {}
@@ -0,0 +1,121 @@
+use crate::{
+ AnyElement, Element, IntoAnyElement, Layout, LayoutId, Line, Pixels, Result, Size, ViewContext,
+};
+use parking_lot::Mutex;
+use std::{marker::PhantomData, sync::Arc};
+use util::{arc_cow::ArcCow, ResultExt};
+
+impl<S: 'static> IntoAnyElement<S> for ArcCow<'static, str> {
+ fn into_any(self) -> AnyElement<S> {
+ Text {
+ text: self,
+ state_type: PhantomData,
+ }
+ .into_any()
+ }
+}
+
+impl<V: 'static> IntoAnyElement<V> for &'static str {
+ fn into_any(self) -> AnyElement<V> {
+ Text {
+ text: ArcCow::from(self),
+ state_type: PhantomData,
+ }
+ .into_any()
+ }
+}
+
+pub struct Text<S> {
+ text: ArcCow<'static, str>,
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static> Element for Text<S> {
+ type State = S;
+ type FrameState = Arc<Mutex<Option<TextLayout>>>;
+
+ fn layout(
+ &mut self,
+ _view: &mut S,
+ cx: &mut ViewContext<S>,
+ ) -> Result<(LayoutId, Self::FrameState)> {
+ dbg!("layout text");
+
+ let text_system = cx.text_system().clone();
+ let text_style = cx.text_style();
+ let font_size = text_style.font_size * cx.rem_size();
+ let line_height = text_style
+ .line_height
+ .to_pixels(font_size.into(), cx.rem_size());
+ let text = self.text.clone();
+ let paint_state = Arc::new(Mutex::new(None));
+
+ let rem_size = cx.rem_size();
+ let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
+ let frame_state = paint_state.clone();
+ move |_, _| {
+ dbg!("starting measurement");
+ let Some(line_layout) = text_system
+ .layout_line(
+ text.as_ref(),
+ font_size,
+ &[(text.len(), text_style.to_run())],
+ )
+ .log_err()
+ else {
+ return Size::default();
+ };
+ dbg!("bbbb");
+
+ let size = Size {
+ width: line_layout.width(),
+ height: line_height,
+ };
+
+ frame_state.lock().replace(TextLayout {
+ line: Arc::new(line_layout),
+ line_height,
+ });
+
+ dbg!(size)
+ }
+ });
+
+ dbg!("got to end of text layout");
+ Ok((layout_id?, paint_state))
+ }
+
+ fn paint<'a>(
+ &mut self,
+ layout: Layout,
+ _: &mut Self::State,
+ paint_state: &mut Self::FrameState,
+ cx: &mut ViewContext<S>,
+ ) -> Result<()> {
+ let bounds = layout.bounds;
+
+ let line;
+ let line_height;
+ {
+ let paint_state = paint_state.lock();
+ let paint_state = paint_state
+ .as_ref()
+ .expect("measurement has not been performed");
+ line = paint_state.line.clone();
+ line_height = paint_state.line_height;
+ }
+
+ let _text_style = cx.text_style();
+
+ // todo!("We haven't added visible bounds to the new element system yet, so this is a placeholder.");
+ let visible_bounds = bounds;
+ line.paint(bounds.origin, visible_bounds, line_height, cx)?;
+
+ Ok(())
+ }
+}
+
+pub struct TextLayout {
+ line: Arc<Line>,
+ line_height: Pixels,
+}
@@ -0,0 +1,1095 @@
+use crate::util;
+use crate::PlatformDispatcher;
+use anyhow::{anyhow, Result};
+use async_task::Runnable;
+use futures::channel::{mpsc, oneshot};
+use smol::{channel, prelude::*, Executor};
+use std::{
+ any::Any,
+ fmt::{self},
+ marker::PhantomData,
+ mem,
+ pin::Pin,
+ rc::Rc,
+ sync::Arc,
+ task::{Context, Poll},
+ thread,
+ time::Duration,
+};
+
+/// Enqueues the given closure to run on the application's event loop.
+/// Returns the result asynchronously.
+pub(crate) fn run_on_main<F, R>(
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ func: F,
+) -> impl Future<Output = R>
+where
+ F: FnOnce() -> R + Send + 'static,
+ R: Send + 'static,
+{
+ spawn_on_main(dispatcher, move || async move { func() })
+}
+
+/// Enqueues the given closure to be run on the application's event loop. The
+/// closure returns a future which will be run to completion on the main thread.
+pub(crate) fn spawn_on_main<F, R>(
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ func: impl FnOnce() -> F + Send + 'static,
+) -> impl Future<Output = R>
+where
+ F: Future<Output = R> + 'static,
+ R: Send + 'static,
+{
+ let (tx, rx) = oneshot::channel();
+ let (runnable, task) = async_task::spawn(
+ {
+ let dispatcher = dispatcher.clone();
+ async move {
+ let future = func();
+ let _ = spawn_on_main_local(dispatcher, async move {
+ let result = future.await;
+ let _ = tx.send(result);
+ });
+ }
+ },
+ move |runnable| dispatcher.run_on_main_thread(runnable),
+ );
+ runnable.schedule();
+ task.detach();
+ async move { rx.await.unwrap() }
+}
+
+/// Enqueues the given closure to be run on the application's event loop. Must
+/// be called on the main thread.
+pub(crate) fn spawn_on_main_local<R>(
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ future: impl Future<Output = R> + 'static,
+) -> impl Future<Output = R>
+where
+ R: 'static,
+{
+ assert!(dispatcher.is_main_thread(), "must be called on main thread");
+
+ let (tx, rx) = oneshot::channel();
+ let (runnable, task) = async_task::spawn_local(
+ async move {
+ let result = future.await;
+ let _ = tx.send(result);
+ },
+ move |runnable| dispatcher.run_on_main_thread(runnable),
+ );
+ runnable.schedule();
+ task.detach();
+ async move { rx.await.unwrap() }
+}
+
+pub enum ForegroundExecutor {
+ Platform {
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ _not_send_or_sync: PhantomData<Rc<()>>,
+ },
+ #[cfg(any(test, feature = "test"))]
+ Deterministic {
+ cx_id: usize,
+ executor: Arc<Deterministic>,
+ },
+}
+
+pub enum BackgroundExecutor {
+ #[cfg(any(test, feature = "test"))]
+ Deterministic { executor: Arc<Deterministic> },
+ Production {
+ executor: Arc<smol::Executor<'static>>,
+ _stop: channel::Sender<()>,
+ },
+}
+
+type AnyLocalFuture = Pin<Box<dyn 'static + Future<Output = Box<dyn Any + 'static>>>>;
+type AnyFuture = Pin<Box<dyn 'static + Send + Future<Output = Box<dyn Any + Send + 'static>>>>;
+type AnyTask = async_task::Task<Box<dyn Any + Send + 'static>>;
+type AnyLocalTask = async_task::Task<Box<dyn Any + 'static>>;
+
+#[must_use]
+pub enum Task<T> {
+ Ready(Option<T>),
+ Local {
+ any_task: AnyLocalTask,
+ result_type: PhantomData<T>,
+ },
+ Send {
+ any_task: AnyTask,
+ result_type: PhantomData<T>,
+ },
+}
+
+unsafe impl<T: Send> Send for Task<T> {}
+
+#[cfg(any(test, feature = "test"))]
+struct DeterministicState {
+ rng: rand::prelude::StdRng,
+ seed: u64,
+ scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>,
+ scheduled_from_background: Vec<BackgroundRunnable>,
+ forbid_parking: bool,
+ block_on_ticks: std::ops::RangeInclusive<usize>,
+ now: std::time::Instant,
+ next_timer_id: usize,
+ pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
+ waiting_backtrace: Option<backtrace::Backtrace>,
+ next_runnable_id: usize,
+ poll_history: Vec<ExecutorEvent>,
+ previous_poll_history: Option<Vec<ExecutorEvent>>,
+ enable_runnable_backtraces: bool,
+ runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ExecutorEvent {
+ PollRunnable { id: usize },
+ EnqueuRunnable { id: usize },
+}
+
+#[cfg(any(test, feature = "test"))]
+struct ForegroundRunnable {
+ id: usize,
+ runnable: Runnable,
+ main: bool,
+}
+
+#[cfg(any(test, feature = "test"))]
+struct BackgroundRunnable {
+ id: usize,
+ runnable: Runnable,
+}
+
+#[cfg(any(test, feature = "test"))]
+pub struct Deterministic {
+ state: Arc<parking_lot::Mutex<DeterministicState>>,
+ parker: parking_lot::Mutex<parking::Parker>,
+}
+
+#[must_use]
+pub enum Timer {
+ Production(smol::Timer),
+ #[cfg(any(test, feature = "test"))]
+ Deterministic(DeterministicTimer),
+}
+
+#[cfg(any(test, feature = "test"))]
+pub struct DeterministicTimer {
+ rx: postage::barrier::Receiver,
+ id: usize,
+ state: Arc<parking_lot::Mutex<DeterministicState>>,
+}
+
+#[cfg(any(test, feature = "test"))]
+impl Deterministic {
+ pub fn new(seed: u64) -> Arc<Self> {
+ use rand::prelude::*;
+
+ Arc::new(Self {
+ state: Arc::new(parking_lot::Mutex::new(DeterministicState {
+ rng: StdRng::seed_from_u64(seed),
+ seed,
+ scheduled_from_foreground: Default::default(),
+ scheduled_from_background: Default::default(),
+ forbid_parking: false,
+ block_on_ticks: 0..=1000,
+ now: std::time::Instant::now(),
+ next_timer_id: Default::default(),
+ pending_timers: Default::default(),
+ waiting_backtrace: None,
+ next_runnable_id: 0,
+ poll_history: Default::default(),
+ previous_poll_history: Default::default(),
+ enable_runnable_backtraces: false,
+ runnable_backtraces: Default::default(),
+ })),
+ parker: Default::default(),
+ })
+ }
+
+ pub fn execution_history(&self) -> Vec<ExecutorEvent> {
+ self.state.lock().poll_history.clone()
+ }
+
+ pub fn set_previous_execution_history(&self, history: Option<Vec<ExecutorEvent>>) {
+ self.state.lock().previous_poll_history = history;
+ }
+
+ pub fn enable_runnable_backtrace(&self) {
+ self.state.lock().enable_runnable_backtraces = true;
+ }
+
+ pub fn runnable_backtrace(&self, runnable_id: usize) -> backtrace::Backtrace {
+ let mut backtrace = self.state.lock().runnable_backtraces[&runnable_id].clone();
+ backtrace.resolve();
+ backtrace
+ }
+
+ pub fn build_background(self: &Arc<Self>) -> Arc<BackgroundExecutor> {
+ Arc::new(BackgroundExecutor::Deterministic {
+ executor: self.clone(),
+ })
+ }
+
+ pub fn build_foreground(self: &Arc<Self>, id: usize) -> Rc<ForegroundExecutor> {
+ Rc::new(ForegroundExecutor::Deterministic {
+ cx_id: id,
+ executor: self.clone(),
+ })
+ }
+
+ fn spawn_from_foreground(
+ &self,
+ cx_id: usize,
+ future: AnyLocalFuture,
+ main: bool,
+ ) -> AnyLocalTask {
+ let state = self.state.clone();
+ let id;
+ {
+ let mut state = state.lock();
+ id = util::post_inc(&mut state.next_runnable_id);
+ if state.enable_runnable_backtraces {
+ state
+ .runnable_backtraces
+ .insert(id, backtrace::Backtrace::new_unresolved());
+ }
+ }
+
+ let unparker = self.parker.lock().unparker();
+ let (runnable, task) = async_task::spawn_local(future, move |runnable| {
+ let mut state = state.lock();
+ state.push_to_history(ExecutorEvent::EnqueuRunnable { id });
+ state
+ .scheduled_from_foreground
+ .entry(cx_id)
+ .or_default()
+ .push(ForegroundRunnable { id, runnable, main });
+ unparker.unpark();
+ });
+ runnable.schedule();
+ task
+ }
+
+ fn spawn(&self, future: AnyFuture) -> AnyTask {
+ let state = self.state.clone();
+ let id;
+ {
+ let mut state = state.lock();
+ id = util::post_inc(&mut state.next_runnable_id);
+ if state.enable_runnable_backtraces {
+ state
+ .runnable_backtraces
+ .insert(id, backtrace::Backtrace::new_unresolved());
+ }
+ }
+
+ let unparker = self.parker.lock().unparker();
+ let (runnable, task) = async_task::spawn(future, move |runnable| {
+ let mut state = state.lock();
+ state
+ .poll_history
+ .push(ExecutorEvent::EnqueuRunnable { id });
+ state
+ .scheduled_from_background
+ .push(BackgroundRunnable { id, runnable });
+ unparker.unpark();
+ });
+ runnable.schedule();
+ task
+ }
+
+ fn run<'a>(
+ &self,
+ cx_id: usize,
+ main_future: Pin<Box<dyn 'a + Future<Output = Box<dyn Any>>>>,
+ ) -> Box<dyn Any> {
+ use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
+
+ let woken = Arc::new(AtomicBool::new(false));
+
+ let state = self.state.clone();
+ let id;
+ {
+ let mut state = state.lock();
+ id = util::post_inc(&mut state.next_runnable_id);
+ if state.enable_runnable_backtraces {
+ state
+ .runnable_backtraces
+ .insert(id, backtrace::Backtrace::new_unresolved());
+ }
+ }
+
+ let unparker = self.parker.lock().unparker();
+ let (runnable, mut main_task) = unsafe {
+ async_task::spawn_unchecked(main_future, move |runnable| {
+ let state = &mut *state.lock();
+ state
+ .scheduled_from_foreground
+ .entry(cx_id)
+ .or_default()
+ .push(ForegroundRunnable {
+ id: util::post_inc(&mut state.next_runnable_id),
+ runnable,
+ main: true,
+ });
+ unparker.unpark();
+ })
+ };
+ runnable.schedule();
+
+ loop {
+ if let Some(result) = self.run_internal(woken.clone(), Some(&mut main_task)) {
+ return result;
+ }
+
+ if !woken.load(SeqCst) {
+ self.state.lock().will_park();
+ }
+
+ woken.store(false, SeqCst);
+ self.parker.lock().park();
+ }
+ }
+
+ pub fn run_until_parked(&self) {
+ use std::sync::atomic::AtomicBool;
+ let woken = Arc::new(AtomicBool::new(false));
+ self.run_internal(woken, None);
+ }
+
+ fn run_internal(
+ &self,
+ woken: Arc<std::sync::atomic::AtomicBool>,
+ mut main_task: Option<&mut AnyLocalTask>,
+ ) -> Option<Box<dyn Any>> {
+ use rand::prelude::*;
+ use std::sync::atomic::Ordering::SeqCst;
+
+ let unparker = self.parker.lock().unparker();
+ let waker = waker_fn::waker_fn(move || {
+ woken.store(true, SeqCst);
+ unparker.unpark();
+ });
+
+ let mut cx = Context::from_waker(&waker);
+ loop {
+ let mut state = self.state.lock();
+
+ if state.scheduled_from_foreground.is_empty()
+ && state.scheduled_from_background.is_empty()
+ {
+ if let Some(main_task) = main_task {
+ if let Poll::Ready(result) = main_task.poll(&mut cx) {
+ return Some(result);
+ }
+ }
+
+ return None;
+ }
+
+ if !state.scheduled_from_background.is_empty() && state.rng.gen() {
+ let background_len = state.scheduled_from_background.len();
+ let ix = state.rng.gen_range(0..background_len);
+ let background_runnable = state.scheduled_from_background.remove(ix);
+ state.push_to_history(ExecutorEvent::PollRunnable {
+ id: background_runnable.id,
+ });
+ drop(state);
+ background_runnable.runnable.run();
+ } else if !state.scheduled_from_foreground.is_empty() {
+ let available_cx_ids = state
+ .scheduled_from_foreground
+ .keys()
+ .copied()
+ .collect::<Vec<_>>();
+ let cx_id_to_run = *available_cx_ids.iter().choose(&mut state.rng).unwrap();
+ let scheduled_from_cx = state
+ .scheduled_from_foreground
+ .get_mut(&cx_id_to_run)
+ .unwrap();
+ let foreground_runnable = scheduled_from_cx.remove(0);
+ if scheduled_from_cx.is_empty() {
+ state.scheduled_from_foreground.remove(&cx_id_to_run);
+ }
+ state.push_to_history(ExecutorEvent::PollRunnable {
+ id: foreground_runnable.id,
+ });
+
+ drop(state);
+
+ foreground_runnable.runnable.run();
+ if let Some(main_task) = main_task.as_mut() {
+ if foreground_runnable.main {
+ if let Poll::Ready(result) = main_task.poll(&mut cx) {
+ return Some(result);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn block<F, T>(&self, future: &mut F, max_ticks: usize) -> Option<T>
+ where
+ F: Unpin + Future<Output = T>,
+ {
+ use rand::prelude::*;
+
+ let unparker = self.parker.lock().unparker();
+ let waker = waker_fn::waker_fn(move || {
+ unparker.unpark();
+ });
+
+ let mut cx = Context::from_waker(&waker);
+ for _ in 0..max_ticks {
+ let mut state = self.state.lock();
+ let runnable_count = state.scheduled_from_background.len();
+ let ix = state.rng.gen_range(0..=runnable_count);
+ if ix < state.scheduled_from_background.len() {
+ let background_runnable = state.scheduled_from_background.remove(ix);
+ state.push_to_history(ExecutorEvent::PollRunnable {
+ id: background_runnable.id,
+ });
+ drop(state);
+ background_runnable.runnable.run();
+ } else {
+ drop(state);
+ if let Poll::Ready(result) = future.poll(&mut cx) {
+ return Some(result);
+ }
+ let mut state = self.state.lock();
+ if state.scheduled_from_background.is_empty() {
+ state.will_park();
+ drop(state);
+ self.parker.lock().park();
+ }
+
+ continue;
+ }
+ }
+
+ None
+ }
+
+ pub fn timer(&self, duration: Duration) -> Timer {
+ let (tx, rx) = postage::barrier::channel();
+ let mut state = self.state.lock();
+ let wakeup_at = state.now + duration;
+ let id = util::post_inc(&mut state.next_timer_id);
+ match state
+ .pending_timers
+ .binary_search_by_key(&wakeup_at, |e| e.1)
+ {
+ Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
+ }
+ let state = self.state.clone();
+ Timer::Deterministic(DeterministicTimer { rx, id, state })
+ }
+
+ pub fn now(&self) -> std::time::Instant {
+ let state = self.state.lock();
+ state.now
+ }
+
+ pub fn advance_clock(&self, duration: Duration) {
+ let new_now = self.state.lock().now + duration;
+ loop {
+ self.run_until_parked();
+ let mut state = self.state.lock();
+
+ if let Some((_, wakeup_time, _)) = state.pending_timers.first() {
+ let wakeup_time = *wakeup_time;
+ if wakeup_time <= new_now {
+ let timer_count = state
+ .pending_timers
+ .iter()
+ .take_while(|(_, t, _)| *t == wakeup_time)
+ .count();
+ state.now = wakeup_time;
+ let timers_to_wake = state
+ .pending_timers
+ .drain(0..timer_count)
+ .collect::<Vec<_>>();
+ drop(state);
+ drop(timers_to_wake);
+ continue;
+ }
+ }
+
+ break;
+ }
+
+ self.state.lock().now = new_now;
+ }
+
+ pub fn start_waiting(&self) {
+ self.state.lock().waiting_backtrace = Some(backtrace::Backtrace::new_unresolved());
+ }
+
+ pub fn finish_waiting(&self) {
+ self.state.lock().waiting_backtrace.take();
+ }
+
+ pub fn forbid_parking(&self) {
+ use rand::prelude::*;
+
+ let mut state = self.state.lock();
+ state.forbid_parking = true;
+ state.rng = StdRng::seed_from_u64(state.seed);
+ }
+
+ pub fn allow_parking(&self) {
+ use rand::prelude::*;
+
+ let mut state = self.state.lock();
+ state.forbid_parking = false;
+ state.rng = StdRng::seed_from_u64(state.seed);
+ }
+
+ pub async fn simulate_random_delay(&self) {
+ use rand::prelude::*;
+ use smol::future::yield_now;
+ if self.state.lock().rng.gen_bool(0.2) {
+ let yields = self.state.lock().rng.gen_range(1..=10);
+ for _ in 0..yields {
+ yield_now().await;
+ }
+ }
+ }
+
+ pub fn record_backtrace(&self) {
+ let mut state = self.state.lock();
+ if state.enable_runnable_backtraces {
+ let current_id = state
+ .poll_history
+ .iter()
+ .rev()
+ .find_map(|event| match event {
+ ExecutorEvent::PollRunnable { id } => Some(*id),
+ _ => None,
+ });
+ if let Some(id) = current_id {
+ state
+ .runnable_backtraces
+ .insert(id, backtrace::Backtrace::new_unresolved());
+ }
+ }
+ }
+}
+
+impl Drop for Timer {
+ fn drop(&mut self) {
+ #[cfg(any(test, feature = "test"))]
+ if let Timer::Deterministic(DeterministicTimer { state, id, .. }) = self {
+ state
+ .lock()
+ .pending_timers
+ .retain(|(timer_id, _, _)| timer_id != id)
+ }
+ }
+}
+
+impl Future for Timer {
+ type Output = ();
+
+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ match &mut *self {
+ #[cfg(any(test, feature = "test"))]
+ Self::Deterministic(DeterministicTimer { rx, .. }) => {
+ use postage::stream::{PollRecv, Stream as _};
+ smol::pin!(rx);
+ match rx.poll_recv(&mut postage::Context::from_waker(cx.waker())) {
+ PollRecv::Ready(()) | PollRecv::Closed => Poll::Ready(()),
+ PollRecv::Pending => Poll::Pending,
+ }
+ }
+ Self::Production(timer) => {
+ smol::pin!(timer);
+ match timer.poll(cx) {
+ Poll::Ready(_) => Poll::Ready(()),
+ Poll::Pending => Poll::Pending,
+ }
+ }
+ }
+ }
+}
+
+#[cfg(any(test, feature = "test"))]
+impl DeterministicState {
+ fn push_to_history(&mut self, event: ExecutorEvent) {
+ use std::fmt::Write as _;
+
+ self.poll_history.push(event);
+ if let Some(prev_history) = &self.previous_poll_history {
+ let ix = self.poll_history.len() - 1;
+ let prev_event = prev_history[ix];
+ if event != prev_event {
+ let mut message = String::new();
+ writeln!(
+ &mut message,
+ "current runnable backtrace:\n{:?}",
+ self.runnable_backtraces.get_mut(&event.id()).map(|trace| {
+ trace.resolve();
+ crate::util::CwdBacktrace(trace)
+ })
+ )
+ .unwrap();
+ writeln!(
+ &mut message,
+ "previous runnable backtrace:\n{:?}",
+ self.runnable_backtraces
+ .get_mut(&prev_event.id())
+ .map(|trace| {
+ trace.resolve();
+ util::CwdBacktrace(trace)
+ })
+ )
+ .unwrap();
+ panic!("detected non-determinism after {ix}. {message}");
+ }
+ }
+ }
+
+ fn will_park(&mut self) {
+ if self.forbid_parking {
+ let mut backtrace_message = String::new();
+ #[cfg(any(test, feature = "test"))]
+ if let Some(backtrace) = self.waiting_backtrace.as_mut() {
+ backtrace.resolve();
+ backtrace_message = format!(
+ "\nbacktrace of waiting future:\n{:?}",
+ util::CwdBacktrace(backtrace)
+ );
+ }
+
+ panic!(
+ "deterministic executor parked after a call to forbid_parking{}",
+ backtrace_message
+ );
+ }
+ }
+}
+
+#[cfg(any(test, feature = "test"))]
+impl ExecutorEvent {
+ pub fn id(&self) -> usize {
+ match self {
+ ExecutorEvent::PollRunnable { id } => *id,
+ ExecutorEvent::EnqueuRunnable { id } => *id,
+ }
+ }
+}
+
+impl ForegroundExecutor {
+ pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Result<Self> {
+ if dispatcher.is_main_thread() {
+ Ok(Self::Platform {
+ dispatcher,
+ _not_send_or_sync: PhantomData,
+ })
+ } else {
+ Err(anyhow!("must be constructed on main thread"))
+ }
+ }
+
+ pub fn spawn<T: 'static>(&self, future: impl Future<Output = T> + 'static) -> Task<T> {
+ let future = any_local_future(future);
+ let any_task = match self {
+ #[cfg(any(test, feature = "test"))]
+ Self::Deterministic { cx_id, executor } => {
+ executor.spawn_from_foreground(*cx_id, future, false)
+ }
+ Self::Platform { dispatcher, .. } => {
+ fn spawn_inner(
+ future: AnyLocalFuture,
+ dispatcher: &Arc<dyn PlatformDispatcher>,
+ ) -> AnyLocalTask {
+ let dispatcher = dispatcher.clone();
+ let schedule =
+ move |runnable: Runnable| dispatcher.run_on_main_thread(runnable);
+ let (runnable, task) = async_task::spawn_local(future, schedule);
+ runnable.schedule();
+ task
+ }
+ spawn_inner(future, dispatcher)
+ }
+ };
+ Task::local(any_task)
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn run<T: 'static>(&self, future: impl Future<Output = T>) -> T {
+ let future = async move { Box::new(future.await) as Box<dyn Any> }.boxed_local();
+ let result = match self {
+ Self::Deterministic { cx_id, executor } => executor.run(*cx_id, future),
+ Self::Platform { .. } => panic!("you can't call run on a platform foreground executor"),
+ };
+ *result.downcast().unwrap()
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn run_until_parked(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.run_until_parked(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn parking_forbidden(&self) -> bool {
+ match self {
+ Self::Deterministic { executor, .. } => executor.state.lock().forbid_parking,
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn start_waiting(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.start_waiting(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn finish_waiting(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.finish_waiting(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn forbid_parking(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.forbid_parking(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn allow_parking(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.allow_parking(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn advance_clock(&self, duration: Duration) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.advance_clock(duration),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.state.lock().block_on_ticks = range,
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+}
+
+impl BackgroundExecutor {
+ pub fn new() -> Self {
+ let executor = Arc::new(Executor::new());
+ let stop = channel::unbounded::<()>();
+
+ for i in 0..2 * num_cpus::get() {
+ let executor = executor.clone();
+ let stop = stop.1.clone();
+ thread::Builder::new()
+ .name(format!("background-executor-{}", i))
+ .spawn(move || smol::block_on(executor.run(stop.recv())))
+ .unwrap();
+ }
+
+ Self::Production {
+ executor,
+ _stop: stop.0,
+ }
+ }
+
+ pub fn num_cpus(&self) -> usize {
+ num_cpus::get()
+ }
+
+ pub fn spawn<T, F>(&self, future: F) -> Task<T>
+ where
+ T: 'static + Send,
+ F: Send + Future<Output = T> + 'static,
+ {
+ let future = any_future(future);
+ let any_task = match self {
+ Self::Production { executor, .. } => executor.spawn(future),
+ #[cfg(any(test, feature = "test"))]
+ Self::Deterministic { executor } => executor.spawn(future),
+ };
+ Task::send(any_task)
+ }
+
+ pub fn block<F, T>(&self, future: F) -> T
+ where
+ F: Future<Output = T>,
+ {
+ smol::pin!(future);
+ match self {
+ Self::Production { .. } => smol::block_on(&mut future),
+ #[cfg(any(test, feature = "test"))]
+ Self::Deterministic { executor, .. } => {
+ executor.block(&mut future, usize::MAX).unwrap()
+ }
+ }
+ }
+
+ pub fn block_with_timeout<F, T>(
+ &self,
+ timeout: Duration,
+ future: F,
+ ) -> Result<T, impl Future<Output = T>>
+ where
+ T: 'static,
+ F: 'static + Unpin + Future<Output = T>,
+ {
+ let mut future = any_local_future(future);
+ if !timeout.is_zero() {
+ let output = match self {
+ Self::Production { .. } => smol::block_on(util::timeout(timeout, &mut future)).ok(),
+ #[cfg(any(test, feature = "test"))]
+ Self::Deterministic { executor, .. } => {
+ use rand::prelude::*;
+ let max_ticks = {
+ let mut state = executor.state.lock();
+ let range = state.block_on_ticks.clone();
+ state.rng.gen_range(range)
+ };
+ executor.block(&mut future, max_ticks)
+ }
+ };
+ if let Some(output) = output {
+ return Ok(*output.downcast().unwrap());
+ }
+ }
+ Err(async { *future.await.downcast().unwrap() })
+ }
+
+ pub async fn scoped<'scope, F>(self: &Arc<Self>, scheduler: F)
+ where
+ F: FnOnce(&mut Scope<'scope>),
+ {
+ let mut scope = Scope::new(self.clone());
+ (scheduler)(&mut scope);
+ let spawned = mem::take(&mut scope.futures)
+ .into_iter()
+ .map(|f| self.spawn(f))
+ .collect::<Vec<_>>();
+ for task in spawned {
+ task.await;
+ }
+ }
+
+ pub fn timer(&self, duration: Duration) -> Timer {
+ match self {
+ BackgroundExecutor::Production { .. } => {
+ Timer::Production(smol::Timer::after(duration))
+ }
+ #[cfg(any(test, feature = "test"))]
+ BackgroundExecutor::Deterministic { executor } => executor.timer(duration),
+ }
+ }
+
+ pub fn now(&self) -> std::time::Instant {
+ match self {
+ BackgroundExecutor::Production { .. } => std::time::Instant::now(),
+ #[cfg(any(test, feature = "test"))]
+ BackgroundExecutor::Deterministic { executor } => executor.now(),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn rng<'a>(&'a self) -> impl 'a + std::ops::DerefMut<Target = rand::prelude::StdRng> {
+ match self {
+ Self::Deterministic { executor, .. } => {
+ parking_lot::lock_api::MutexGuard::map(executor.state.lock(), |s| &mut s.rng)
+ }
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub async fn simulate_random_delay(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => {
+ executor.simulate_random_delay().await;
+ }
+ _ => {
+ panic!("this method can only be called on a deterministic executor")
+ }
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn record_backtrace(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.record_backtrace(),
+ _ => {
+ panic!("this method can only be called on a deterministic executor")
+ }
+ }
+ }
+
+ #[cfg(any(test, feature = "test"))]
+ pub fn start_waiting(&self) {
+ match self {
+ Self::Deterministic { executor, .. } => executor.start_waiting(),
+ _ => panic!("this method can only be called on a deterministic executor"),
+ }
+ }
+}
+
+impl Default for BackgroundExecutor {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+pub struct Scope<'a> {
+ executor: Arc<BackgroundExecutor>,
+ futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
+ tx: Option<mpsc::Sender<()>>,
+ rx: mpsc::Receiver<()>,
+ _phantom: PhantomData<&'a ()>,
+}
+
+impl<'a> Scope<'a> {
+ fn new(executor: Arc<BackgroundExecutor>) -> Self {
+ let (tx, rx) = mpsc::channel(1);
+ Self {
+ executor,
+ tx: Some(tx),
+ rx,
+ futures: Default::default(),
+ _phantom: PhantomData,
+ }
+ }
+
+ pub fn spawn<F>(&mut self, f: F)
+ where
+ F: Future<Output = ()> + 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<Box<dyn Future<Output = ()> + Send + 'a>>,
+ Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
+ >(Box::pin(async move {
+ f.await;
+ drop(tx);
+ }))
+ };
+ self.futures.push(f);
+ }
+}
+
+impl<'a> Drop for Scope<'a> {
+ fn drop(&mut self) {
+ self.tx.take().unwrap();
+
+ // Wait until the channel is closed, which means that all of the spawned
+ // futures have resolved.
+ self.executor.block(self.rx.next());
+ }
+}
+
+impl<T> Task<T> {
+ pub fn ready(value: T) -> Self {
+ Self::Ready(Some(value))
+ }
+
+ fn local(any_task: AnyLocalTask) -> Self {
+ Self::Local {
+ any_task,
+ result_type: PhantomData,
+ }
+ }
+
+ pub fn detach(self) {
+ match self {
+ Task::Ready(_) => {}
+ Task::Local { any_task, .. } => any_task.detach(),
+ Task::Send { any_task, .. } => any_task.detach(),
+ }
+ }
+}
+
+// impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
+// #[track_caller]
+// pub fn detach_and_log_err(self, cx: &mut AppContext) {
+// let caller = Location::caller();
+// cx.spawn(|_| async move {
+// if let Err(err) = self.await {
+// log::error!("{}:{}: {:#}", caller.file(), caller.line(), err);
+// }
+// })
+// .detach();
+// }
+// }
+
+impl<T: Send> Task<T> {
+ fn send(any_task: AnyTask) -> Self {
+ Self::Send {
+ any_task,
+ result_type: PhantomData,
+ }
+ }
+}
+
+impl<T: fmt::Debug> fmt::Debug for Task<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Task::Ready(value) => value.fmt(f),
+ Task::Local { any_task, .. } => any_task.fmt(f),
+ Task::Send { any_task, .. } => any_task.fmt(f),
+ }
+ }
+}
+
+impl<T: 'static> Future for Task<T> {
+ type Output = T;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ match unsafe { self.get_unchecked_mut() } {
+ Task::Ready(value) => Poll::Ready(value.take().unwrap()),
+ Task::Local { any_task, .. } => {
+ any_task.poll(cx).map(|value| *value.downcast().unwrap())
+ }
+ Task::Send { any_task, .. } => {
+ any_task.poll(cx).map(|value| *value.downcast().unwrap())
+ }
+ }
+ }
+}
+
+fn any_future<T, F>(future: F) -> AnyFuture
+where
+ T: 'static + Send,
+ F: Future<Output = T> + Send + 'static,
+{
+ async { Box::new(future.await) as Box<dyn Any + Send> }.boxed()
+}
+
+fn any_local_future<T, F>(future: F) -> AnyLocalFuture
+where
+ T: 'static,
+ F: Future<Output = T> + 'static,
+{
+ async { Box::new(future.await) as Box<dyn Any> }.boxed_local()
+}
@@ -0,0 +1,723 @@
+use bytemuck::{Pod, Zeroable};
+use core::fmt::Debug;
+use derive_more::{Add, AddAssign, Div, Mul, Sub, SubAssign};
+use refineable::Refineable;
+use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub, SubAssign};
+
+#[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Point<T: Clone + Debug> {
+ pub x: T,
+ pub y: T,
+}
+
+pub fn point<T: Clone + Debug>(x: T, y: T) -> Point<T> {
+ Point { x, y }
+}
+
+impl<T: Clone + Debug> Point<T> {
+ pub fn new(x: T, y: T) -> Self {
+ Self { x, y }
+ }
+
+ pub fn map<U: Clone + Debug, F: Fn(T) -> U>(&self, f: F) -> Point<U> {
+ Point {
+ x: f(self.x.clone()),
+ y: f(self.y.clone()),
+ }
+ }
+}
+
+impl<T, Rhs> Mul<Rhs> for Point<T>
+where
+ T: Mul<Rhs, Output = Rhs> + Clone + Debug,
+ Rhs: Clone + Debug,
+{
+ type Output = Point<Rhs>;
+
+ fn mul(self, rhs: Rhs) -> Self::Output {
+ Point {
+ x: self.x * rhs.clone(),
+ y: self.y * rhs,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Mul<S, Output = T>, S: Clone> MulAssign<S> for Point<T> {
+ fn mul_assign(&mut self, rhs: S) {
+ self.x = self.x.clone() * rhs.clone();
+ self.y = self.y.clone() * rhs;
+ }
+}
+
+impl<T: Clone + Debug + Sub<Output = T>> SubAssign<Size<T>> for Point<T> {
+ fn sub_assign(&mut self, rhs: Size<T>) {
+ self.x = self.x.clone() - rhs.width;
+ self.y = self.y.clone() - rhs.height;
+ }
+}
+
+impl<T: Clone + Debug + Add<Output = T> + Copy> AddAssign<T> for Point<T> {
+ fn add_assign(&mut self, rhs: T) {
+ self.x = self.x.clone() + rhs;
+ self.y = self.y.clone() + rhs;
+ }
+}
+
+impl<T: Clone + Debug + Div<S, Output = T>, S: Clone> Div<S> for Point<T> {
+ type Output = Self;
+
+ fn div(self, rhs: S) -> Self::Output {
+ Self {
+ x: self.x / rhs.clone(),
+ y: self.y / rhs,
+ }
+ }
+}
+
+impl<T: Clone + Debug + std::cmp::PartialOrd> Point<T> {
+ 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()
+ },
+ }
+ }
+}
+
+impl<T: Clone + Debug> Clone for Point<T> {
+ fn clone(&self) -> Self {
+ Self {
+ x: self.x.clone(),
+ y: self.y.clone(),
+ }
+ }
+}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Zeroable for Point<T> {}
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Pod for Point<T> {}
+
+#[derive(Refineable, Default, Clone, Copy, Debug, PartialEq, Div)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Size<T: Clone + Debug> {
+ pub width: T,
+ pub height: T,
+}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Zeroable for Size<T> {}
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Pod for Size<T> {}
+
+pub fn size<T: Clone + Debug>(width: T, height: T) -> Size<T> {
+ Size { width, height }
+}
+
+impl<T: Clone + Debug> Size<T> {
+ pub fn map<U: Clone + Debug, F: Fn(T) -> U>(&self, f: F) -> Size<U> {
+ Size {
+ width: f(self.width.clone()),
+ height: f(self.height.clone()),
+ }
+ }
+}
+
+impl<T, Rhs> Mul<Rhs> for Size<T>
+where
+ T: Mul<Rhs, Output = Rhs> + Debug + Clone,
+ Rhs: Debug + Clone,
+{
+ type Output = Size<Rhs>;
+
+ fn mul(self, rhs: Rhs) -> Self::Output {
+ Size {
+ width: self.width * rhs.clone(),
+ height: self.height * rhs,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Mul<S, Output = T>, S: Clone> MulAssign<S> for Size<T> {
+ fn mul_assign(&mut self, rhs: S) {
+ self.width = self.width.clone() * rhs.clone();
+ self.height = self.height.clone() * rhs;
+ }
+}
+
+impl From<Size<Option<Pixels>>> for Size<Option<f32>> {
+ fn from(val: Size<Option<Pixels>>) -> Self {
+ Size {
+ width: val.width.map(|p| p.0 as f32),
+ height: val.height.map(|p| p.0 as f32),
+ }
+ }
+}
+
+impl Size<Length> {
+ pub fn full() -> Self {
+ Self {
+ width: relative(1.).into(),
+ height: relative(1.).into(),
+ }
+ }
+}
+
+impl Size<DefiniteLength> {
+ pub fn zero() -> Self {
+ Self {
+ width: px(0.).into(),
+ height: px(0.).into(),
+ }
+ }
+}
+
+impl Size<Length> {
+ pub fn auto() -> Self {
+ Self {
+ width: Length::Auto,
+ height: Length::Auto,
+ }
+ }
+}
+
+#[derive(Refineable, Clone, Default, Debug, PartialEq)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Bounds<T: Clone + Debug> {
+ pub origin: Point<T>,
+ pub size: Size<T>,
+}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Zeroable for Bounds<T> {}
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Pod for Bounds<T> {}
+
+// Bounds<f32> * Pixels = Bounds<Pixels>
+impl<T, Rhs> Mul<Rhs> for Bounds<T>
+where
+ T: Mul<Rhs, Output = Rhs> + Clone + Debug,
+ Rhs: Clone + Debug,
+{
+ type Output = Bounds<Rhs>;
+
+ fn mul(self, rhs: Rhs) -> Self::Output {
+ Bounds {
+ origin: self.origin * rhs.clone(),
+ size: self.size * rhs,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Mul<S, Output = T>, S: Clone> MulAssign<S> for Bounds<T> {
+ fn mul_assign(&mut self, rhs: S) {
+ self.origin *= rhs.clone();
+ self.size *= rhs;
+ }
+}
+
+impl<T: Clone + Debug + Div<S, Output = T>, S: Clone> Div<S> for Bounds<T>
+where
+ Size<T>: Div<S, Output = Size<T>>,
+{
+ type Output = Self;
+
+ fn div(self, rhs: S) -> Self {
+ Self {
+ origin: self.origin / rhs.clone(),
+ size: self.size / rhs,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Add<T, Output = T>> Bounds<T> {
+ pub fn upper_right(&self) -> Point<T> {
+ Point {
+ x: self.origin.x.clone() + self.size.width.clone(),
+ y: self.origin.y.clone(),
+ }
+ }
+
+ pub fn lower_right(&self) -> Point<T> {
+ Point {
+ x: self.origin.x.clone() + self.size.width.clone(),
+ y: self.origin.y.clone() + self.size.height.clone(),
+ }
+ }
+}
+
+impl<T: Clone + Debug + PartialOrd + Add<T, Output = T>> Bounds<T> {
+ pub fn contains_point(&self, point: Point<T>) -> 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()
+ }
+
+ pub fn map<U: Clone + Debug, F: Fn(T) -> U>(&self, f: F) -> Bounds<U> {
+ Bounds {
+ origin: self.origin.map(&f),
+ size: self.size.map(f),
+ }
+ }
+}
+
+impl<T: Clone + Debug + Copy> Copy for Bounds<T> {}
+
+#[derive(Refineable, Clone, Default, Debug)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Edges<T: Clone + Debug> {
+ pub top: T,
+ pub right: T,
+ pub bottom: T,
+ pub left: T,
+}
+
+impl<T: Clone + Debug + Mul<Output = T>> Mul for Edges<T> {
+ 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.clone() * rhs.left,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Mul<S, Output = T>, S: Clone> MulAssign<S> for Edges<T> {
+ 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.clone();
+ }
+}
+
+impl<T: Clone + Debug + Copy> Copy for Edges<T> {}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Zeroable for Edges<T> {}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Pod for Edges<T> {}
+
+impl<T: Clone + Debug> Edges<T> {
+ pub fn map<U: Clone + Debug, F: Fn(&T) -> U>(&self, f: F) -> Edges<U> {
+ Edges {
+ top: f(&self.top),
+ right: f(&self.right),
+ bottom: f(&self.bottom),
+ left: f(&self.left),
+ }
+ }
+
+ pub fn any<F: Fn(&T) -> bool>(&self, predicate: F) -> bool {
+ predicate(&self.top)
+ || predicate(&self.right)
+ || predicate(&self.bottom)
+ || predicate(&self.left)
+ }
+}
+
+impl Edges<Length> {
+ pub fn auto() -> Self {
+ Self {
+ top: Length::Auto,
+ right: Length::Auto,
+ bottom: Length::Auto,
+ left: Length::Auto,
+ }
+ }
+
+ pub fn zero() -> Self {
+ Self {
+ top: px(0.).into(),
+ right: px(0.).into(),
+ bottom: px(0.).into(),
+ left: px(0.).into(),
+ }
+ }
+}
+
+impl Edges<DefiniteLength> {
+ pub fn zero() -> Self {
+ Self {
+ top: px(0.).into(),
+ right: px(0.).into(),
+ bottom: px(0.).into(),
+ left: px(0.).into(),
+ }
+ }
+}
+
+impl Edges<AbsoluteLength> {
+ pub fn zero() -> Self {
+ Self {
+ top: px(0.).into(),
+ right: px(0.).into(),
+ bottom: px(0.).into(),
+ left: px(0.).into(),
+ }
+ }
+
+ pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> {
+ 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),
+ }
+ }
+}
+
+#[derive(Refineable, Clone, Default, Debug)]
+#[refineable(debug)]
+#[repr(C)]
+pub struct Corners<T: Clone + Debug> {
+ pub top_left: T,
+ pub top_right: T,
+ pub bottom_right: T,
+ pub bottom_left: T,
+}
+
+impl<T: Clone + Debug> Corners<T> {
+ pub fn map<U: Clone + Debug, F: Fn(&T) -> U>(&self, f: F) -> Corners<U> {
+ 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<T: Clone + Debug + Mul<Output = T>> Mul for Corners<T> {
+ 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.clone() * rhs.bottom_left,
+ }
+ }
+}
+
+impl<T: Clone + Debug + Mul<S, Output = T>, S: Clone> MulAssign<S> for Corners<T> {
+ 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<T: Clone + Debug + Copy> Copy for Corners<T> {}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Zeroable for Corners<T> {}
+
+unsafe impl<T: Clone + Debug + Zeroable + Pod> Pod for Corners<T> {}
+
+#[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)]
+#[repr(transparent)]
+pub struct Pixels(pub(crate) f32);
+
+impl Mul<f32> for Pixels {
+ type Output = Pixels;
+
+ fn mul(self, other: f32) -> Pixels {
+ Pixels(self.0 * other)
+ }
+}
+
+impl Mul<Pixels> for f32 {
+ type Output = Pixels;
+
+ fn mul(self, rhs: Pixels) -> Self::Output {
+ Pixels(self * rhs.0)
+ }
+}
+
+impl Pixels {
+ pub fn round(&self) -> Self {
+ Self(self.0.round())
+ }
+
+ pub fn to_device_pixels(&self, scale: f32) -> DevicePixels {
+ DevicePixels((self.0 * scale).ceil() as u32)
+ }
+}
+
+impl Mul<Pixels> for Pixels {
+ type Output = Pixels;
+
+ fn mul(self, rhs: Pixels) -> Self::Output {
+ Pixels(self.0 * rhs.0)
+ }
+}
+
+impl Eq for Pixels {}
+
+impl Ord for Pixels {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.0.partial_cmp(&other.0).unwrap()
+ }
+}
+
+impl std::hash::Hash for Pixels {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.0.to_bits().hash(state);
+ }
+}
+
+impl From<f64> for Pixels {
+ fn from(val: f64) -> Self {
+ Pixels(val as f32)
+ }
+}
+
+impl From<f32> for Pixels {
+ fn from(val: f32) -> Self {
+ Pixels(val)
+ }
+}
+
+unsafe impl bytemuck::Pod for Pixels {}
+unsafe impl bytemuck::Zeroable for Pixels {}
+
+impl Debug for Pixels {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{} px", self.0)
+ }
+}
+
+impl From<Pixels> for f32 {
+ fn from(pixels: Pixels) -> Self {
+ pixels.0
+ }
+}
+
+impl From<&Pixels> for f32 {
+ fn from(pixels: &Pixels) -> Self {
+ pixels.0
+ }
+}
+
+impl From<Pixels> for f64 {
+ fn from(pixels: Pixels) -> Self {
+ pixels.0 as f64
+ }
+}
+
+#[derive(
+ Clone, Copy, Debug, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd,
+)]
+#[repr(transparent)]
+pub struct DevicePixels(pub(crate) u32);
+
+unsafe impl bytemuck::Pod for DevicePixels {}
+unsafe impl bytemuck::Zeroable for DevicePixels {}
+
+impl From<DevicePixels> for u32 {
+ fn from(device_pixels: DevicePixels) -> Self {
+ device_pixels.0
+ }
+}
+
+impl From<u32> for DevicePixels {
+ fn from(val: u32) -> Self {
+ DevicePixels(val)
+ }
+}
+
+#[derive(Clone, Copy, Default, Add, Sub, Mul, Div)]
+pub struct Rems(f32);
+
+impl Mul<Pixels> for Rems {
+ type Output = Pixels;
+
+ fn mul(self, other: Pixels) -> Pixels {
+ Pixels(self.0 * other.0)
+ }
+}
+
+impl Debug for Rems {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{} rem", self.0)
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum AbsoluteLength {
+ Pixels(Pixels),
+ Rems(Rems),
+}
+
+impl AbsoluteLength {
+ pub fn is_zero(&self) -> bool {
+ match self {
+ AbsoluteLength::Pixels(px) => px.0 == 0.,
+ AbsoluteLength::Rems(rems) => rems.0 == 0.,
+ }
+ }
+}
+
+impl From<Pixels> for AbsoluteLength {
+ fn from(pixels: Pixels) -> Self {
+ AbsoluteLength::Pixels(pixels)
+ }
+}
+
+impl From<Rems> for AbsoluteLength {
+ fn from(rems: Rems) -> Self {
+ AbsoluteLength::Rems(rems)
+ }
+}
+
+impl AbsoluteLength {
+ pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
+ match self {
+ AbsoluteLength::Pixels(pixels) => *pixels,
+ AbsoluteLength::Rems(rems) => *rems * rem_size,
+ }
+ }
+}
+
+impl Default for AbsoluteLength {
+ fn default() -> Self {
+ px(0.).into()
+ }
+}
+
+/// A non-auto length that can be defined in pixels, rems, or percent of parent.
+#[derive(Clone, Copy)]
+pub enum DefiniteLength {
+ Absolute(AbsoluteLength),
+ /// A fraction of the parent's size between 0 and 1.
+ Fraction(f32),
+}
+
+impl DefiniteLength {
+ 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 std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ DefiniteLength::Absolute(length) => Debug::fmt(length, f),
+ DefiniteLength::Fraction(fract) => write!(f, "{}%", (fract * 100.0) as i32),
+ }
+ }
+}
+
+impl From<Pixels> for DefiniteLength {
+ fn from(pixels: Pixels) -> Self {
+ Self::Absolute(pixels.into())
+ }
+}
+
+impl From<Rems> for DefiniteLength {
+ fn from(rems: Rems) -> Self {
+ Self::Absolute(rems.into())
+ }
+}
+
+impl From<AbsoluteLength> 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)]
+pub enum Length {
+ Definite(DefiniteLength),
+ Auto,
+}
+
+impl Debug for Length {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Length::Definite(definite_length) => write!(f, "{:?}", definite_length),
+ Length::Auto => write!(f, "auto"),
+ }
+ }
+}
+
+pub fn relative(fraction: f32) -> DefiniteLength {
+ DefiniteLength::Fraction(fraction).into()
+}
+
+/// Returns the Golden Ratio, i.e. `~(1.0 + sqrt(5.0)) / 2.0`.
+pub fn phi() -> DefiniteLength {
+ relative(1.61803398875)
+}
+
+pub fn rems(rems: f32) -> Rems {
+ Rems(rems)
+}
+
+pub fn px(pixels: f32) -> Pixels {
+ Pixels(pixels)
+}
+
+pub fn auto() -> Length {
+ Length::Auto
+}
+
+impl From<Pixels> for Length {
+ fn from(pixels: Pixels) -> Self {
+ Self::Definite(pixels.into())
+ }
+}
+
+impl From<Rems> for Length {
+ fn from(rems: Rems) -> Self {
+ Self::Definite(rems.into())
+ }
+}
+
+impl From<DefiniteLength> for Length {
+ fn from(length: DefiniteLength) -> Self {
+ Self::Definite(length)
+ }
+}
+
+impl From<AbsoluteLength> 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())
+ }
+}
@@ -0,0 +1,207 @@
+mod app;
+mod color;
+mod element;
+mod elements;
+mod executor;
+mod geometry;
+mod platform;
+mod scene;
+mod style;
+mod style_helpers;
+mod styled;
+mod taffy;
+mod text_system;
+mod util;
+mod view;
+mod window;
+
+pub use anyhow::Result;
+pub use app::*;
+pub use color::*;
+pub use element::*;
+pub use elements::*;
+pub use executor::*;
+pub use geometry::*;
+pub use gpui3_macros::*;
+pub use platform::*;
+pub use refineable::*;
+pub use scene::*;
+pub use serde;
+pub use serde_json;
+pub use smallvec;
+pub use smol::Timer;
+use std::{
+ ops::{Deref, DerefMut},
+ sync::Arc,
+};
+pub use style::*;
+pub use style_helpers::*;
+pub use styled::*;
+use taffy::TaffyLayoutEngine;
+pub use taffy::{AvailableSpace, LayoutId};
+pub use text_system::*;
+pub use util::arc_cow::ArcCow;
+pub use view::*;
+pub use window::*;
+
+pub trait Context {
+ type EntityContext<'a, 'w, T: 'static + Send + Sync>;
+ type Result<T>;
+
+ fn entity<T: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+ ) -> Self::Result<Handle<T>>;
+
+ fn update_entity<T: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<T>,
+ update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+ ) -> Self::Result<R>;
+}
+
+#[repr(transparent)]
+pub struct MainThread<T>(T);
+
+impl<T> MainThread<T> {
+ fn new(value: T) -> Self {
+ Self(value)
+ }
+}
+
+impl<T> Deref for MainThread<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T> DerefMut for MainThread<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+pub trait StackContext {
+ fn app(&mut self) -> &mut AppContext;
+
+ fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.app().push_text_style(style);
+ let result = f(self);
+ self.app().pop_text_style();
+ result
+ }
+
+ fn with_state<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.app().push_state(state);
+ let result = f(self);
+ self.app().pop_state::<T>();
+ result
+ }
+}
+
+pub trait Flatten<T> {
+ fn flatten(self) -> Result<T>;
+}
+
+impl<T> Flatten<T> for Result<Result<T>> {
+ fn flatten(self) -> Result<T> {
+ self?
+ }
+}
+
+impl<T> Flatten<T> for Result<T> {
+ fn flatten(self) -> Result<T> {
+ self
+ }
+}
+
+#[derive(Clone, Eq, PartialEq, Hash)]
+pub struct SharedString(ArcCow<'static, str>);
+
+impl Default for SharedString {
+ fn default() -> Self {
+ Self(ArcCow::Owned("".into()))
+ }
+}
+
+impl AsRef<str> for SharedString {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl std::fmt::Debug for SharedString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
+ fn from(value: T) -> Self {
+ Self(value.into())
+ }
+}
+
+pub enum Reference<'a, T> {
+ Immutable(&'a T),
+ Mutable(&'a mut T),
+}
+
+impl<'a, T> Deref for Reference<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ match self {
+ Reference::Immutable(target) => target,
+ Reference::Mutable(target) => target,
+ }
+ }
+}
+
+impl<'a, T> DerefMut for Reference<'a, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ match self {
+ Reference::Immutable(_) => {
+ panic!("cannot mutably deref an immutable reference. this is a bug in GPUI.");
+ }
+ Reference::Mutable(target) => target,
+ }
+ }
+}
+
+pub(crate) struct MainThreadOnly<T: ?Sized> {
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ value: Arc<T>,
+}
+
+impl<T: ?Sized> Clone for MainThreadOnly<T> {
+ fn clone(&self) -> Self {
+ Self {
+ dispatcher: self.dispatcher.clone(),
+ value: self.value.clone(),
+ }
+ }
+}
+
+/// Allows a value to be accessed only on the main thread, allowing a non-`Send` type
+/// to become `Send`.
+impl<T: 'static + ?Sized> MainThreadOnly<T> {
+ pub(crate) fn new(value: Arc<T>, dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
+ Self { dispatcher, value }
+ }
+
+ pub(crate) fn borrow_on_main_thread(&self) -> &T {
+ assert!(self.dispatcher.is_main_thread());
+ &self.value
+ }
+}
+
+unsafe impl<T: ?Sized> Send for MainThreadOnly<T> {}
@@ -0,0 +1,399 @@
+mod events;
+mod keystroke;
+#[cfg(target_os = "macos")]
+mod mac;
+#[cfg(any(test, feature = "test"))]
+mod test;
+
+use crate::{
+ AnyWindowHandle, Bounds, Font, FontId, FontMetrics, GlyphId, LineLayout, Pixels, Point, Result,
+ Scene, SharedString, Size,
+};
+use anyhow::anyhow;
+use async_task::Runnable;
+use futures::channel::oneshot;
+use seahash::SeaHasher;
+use serde::{Deserialize, Serialize};
+use std::ffi::c_void;
+use std::hash::{Hash, Hasher};
+use std::{
+ any::Any,
+ fmt::{self, Debug, Display},
+ ops::Range,
+ path::{Path, PathBuf},
+ rc::Rc,
+ str::FromStr,
+ sync::Arc,
+};
+use uuid::Uuid;
+
+pub use events::*;
+pub use keystroke::*;
+#[cfg(target_os = "macos")]
+pub use mac::*;
+#[cfg(any(test, feature = "test"))]
+pub use test::*;
+pub use time::UtcOffset;
+
+#[cfg(target_os = "macos")]
+pub(crate) fn current_platform() -> Arc<dyn Platform> {
+ Arc::new(MacPlatform::new())
+}
+
+pub trait Platform: 'static {
+ fn dispatcher(&self) -> Arc<dyn PlatformDispatcher>;
+ fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
+
+ fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
+ fn quit(&self);
+ fn restart(&self);
+ fn activate(&self, ignoring_other_apps: bool);
+ fn hide(&self);
+ fn hide_other_apps(&self);
+ fn unhide_other_apps(&self);
+
+ fn screens(&self) -> Vec<Rc<dyn PlatformScreen>>;
+ fn screen_by_id(&self, id: ScreenId) -> Option<Rc<dyn PlatformScreen>>;
+ fn main_window(&self) -> Option<AnyWindowHandle>;
+ fn open_window(
+ &self,
+ handle: AnyWindowHandle,
+ options: WindowOptions,
+ ) -> Box<dyn PlatformWindow>;
+ // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn PlatformWindow>;
+
+ fn open_url(&self, url: &str);
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
+ fn prompt_for_paths(
+ &self,
+ options: PathPromptOptions,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
+ fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
+ fn reveal_path(&self, path: &Path);
+
+ fn on_become_active(&self, callback: Box<dyn FnMut()>);
+ fn on_resign_active(&self, callback: Box<dyn FnMut()>);
+ fn on_quit(&self, callback: Box<dyn FnMut()>);
+ fn on_reopen(&self, callback: Box<dyn FnMut()>);
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
+
+ fn os_name(&self) -> &'static str;
+ fn os_version(&self) -> Result<SemanticVersion>;
+ fn app_version(&self) -> Result<SemanticVersion>;
+ fn app_path(&self) -> Result<PathBuf>;
+ fn local_timezone(&self) -> UtcOffset;
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
+
+ fn set_cursor_style(&self, style: CursorStyle);
+ fn should_auto_hide_scrollbars(&self) -> bool;
+
+ fn write_to_clipboard(&self, item: ClipboardItem);
+ fn read_from_clipboard(&self) -> Option<ClipboardItem>;
+
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()>;
+ fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>>;
+ fn delete_credentials(&self, url: &str) -> Result<()>;
+}
+
+pub trait PlatformScreen: Debug {
+ fn id(&self) -> Option<ScreenId>;
+ fn handle(&self) -> PlatformScreenHandle;
+ fn as_any(&self) -> &dyn Any;
+ fn bounds(&self) -> Bounds<Pixels>;
+ fn content_bounds(&self) -> Bounds<Pixels>;
+}
+
+pub struct PlatformScreenHandle(pub *mut c_void);
+
+impl Debug for PlatformScreenHandle {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "PlatformScreenHandle({:p})", self.0)
+ }
+}
+
+unsafe impl Send for PlatformScreenHandle {}
+
+pub trait PlatformWindow {
+ fn bounds(&self) -> WindowBounds;
+ fn content_size(&self) -> Size<Pixels>;
+ fn scale_factor(&self) -> f32;
+ fn titlebar_height(&self) -> Pixels;
+ fn appearance(&self) -> WindowAppearance;
+ fn screen(&self) -> Rc<dyn PlatformScreen>;
+ fn mouse_position(&self) -> Point<Pixels>;
+ fn as_any_mut(&mut self) -> &mut dyn Any;
+ fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
+ fn prompt(
+ &self,
+ level: WindowPromptLevel,
+ msg: &str,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize>;
+ fn activate(&self);
+ fn set_title(&mut self, title: &str);
+ fn set_edited(&mut self, edited: bool);
+ fn show_character_palette(&self);
+ fn minimize(&self);
+ fn zoom(&self);
+ fn toggle_full_screen(&self);
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
+ fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
+ fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
+ fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);
+ fn on_moved(&self, callback: Box<dyn FnMut()>);
+ fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
+ fn on_close(&self, callback: Box<dyn FnOnce()>);
+ fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
+ fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
+ fn draw(&self, scene: Scene);
+}
+
+pub trait PlatformDispatcher: Send + Sync {
+ fn is_main_thread(&self) -> bool;
+ fn run_on_main_thread(&self, task: Runnable);
+}
+
+pub trait PlatformTextSystem: Send + Sync {
+ fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
+ fn all_font_families(&self) -> Vec<String>;
+ fn font_id(&self, descriptor: &Font) -> Result<FontId>;
+ fn font_metrics(&self, font_id: FontId) -> FontMetrics;
+ fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
+ fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
+ fn rasterize_glyph(
+ &self,
+ font_id: FontId,
+ font_size: f32,
+ glyph_id: GlyphId,
+ subpixel_shift: Point<Pixels>,
+ scale_factor: f32,
+ options: RasterizationOptions,
+ ) -> Option<(Bounds<u32>, Vec<u8>)>;
+ fn layout_line(&self, text: &str, font_size: Pixels, runs: &[(usize, FontId)]) -> LineLayout;
+ fn wrap_line(
+ &self,
+ text: &str,
+ font_id: FontId,
+ font_size: Pixels,
+ width: Pixels,
+ ) -> Vec<usize>;
+}
+
+pub trait InputHandler {
+ fn selected_text_range(&self) -> Option<Range<usize>>;
+ fn marked_text_range(&self) -> Option<Range<usize>>;
+ fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
+ fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
+ fn replace_and_mark_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ new_text: &str,
+ new_selected_range: Option<Range<usize>>,
+ );
+ fn unmark_text(&mut self);
+ fn bounds_for_range(&self, range_utf16: Range<usize>) -> Option<Bounds<f32>>;
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct ScreenId(pub(crate) Uuid);
+
+#[derive(Copy, Clone, Debug)]
+pub enum RasterizationOptions {
+ Alpha,
+ Bgra,
+}
+
+#[derive(Debug)]
+pub struct WindowOptions {
+ pub bounds: WindowBounds,
+ pub titlebar: Option<TitlebarOptions>,
+ pub center: bool,
+ pub focus: bool,
+ pub show: bool,
+ pub kind: WindowKind,
+ pub is_movable: bool,
+ pub screen: Option<PlatformScreenHandle>,
+}
+
+impl Default for WindowOptions {
+ fn default() -> Self {
+ Self {
+ bounds: WindowBounds::default(),
+ titlebar: Some(TitlebarOptions {
+ title: Default::default(),
+ appears_transparent: Default::default(),
+ traffic_light_position: Default::default(),
+ }),
+ center: false,
+ focus: true,
+ show: true,
+ kind: WindowKind::Normal,
+ is_movable: true,
+ screen: None,
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct TitlebarOptions {
+ pub title: Option<SharedString>,
+ pub appears_transparent: bool,
+ pub traffic_light_position: Option<Point<Pixels>>,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Appearance {
+ Light,
+ VibrantLight,
+ Dark,
+ VibrantDark,
+}
+
+impl Default for Appearance {
+ fn default() -> Self {
+ Self::Light
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum WindowKind {
+ Normal,
+ PopUp,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default)]
+pub enum WindowBounds {
+ Fullscreen,
+ #[default]
+ Maximized,
+ Fixed(Bounds<Pixels>),
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum WindowAppearance {
+ Light,
+ VibrantLight,
+ Dark,
+ VibrantDark,
+}
+
+impl Default for WindowAppearance {
+ fn default() -> Self {
+ Self::Light
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default)]
+pub enum WindowPromptLevel {
+ #[default]
+ Info,
+ Warning,
+ Critical,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct PathPromptOptions {
+ pub files: bool,
+ pub directories: bool,
+ pub multiple: bool,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum PromptLevel {
+ Info,
+ Warning,
+ Critical,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum CursorStyle {
+ Arrow,
+ ResizeLeftRight,
+ ResizeUpDown,
+ PointingHand,
+ IBeam,
+}
+
+impl Default for CursorStyle {
+ fn default() -> Self {
+ Self::Arrow
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct SemanticVersion {
+ major: usize,
+ minor: usize,
+ patch: usize,
+}
+
+impl FromStr for SemanticVersion {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self> {
+ let mut components = s.trim().split('.');
+ let major = components
+ .next()
+ .ok_or_else(|| anyhow!("missing major version number"))?
+ .parse()?;
+ let minor = components
+ .next()
+ .ok_or_else(|| anyhow!("missing minor version number"))?
+ .parse()?;
+ let patch = components
+ .next()
+ .ok_or_else(|| anyhow!("missing patch version number"))?
+ .parse()?;
+ Ok(Self {
+ major,
+ minor,
+ patch,
+ })
+ }
+}
+
+impl Display for SemanticVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ClipboardItem {
+ pub(crate) text: String,
+ pub(crate) metadata: Option<String>,
+}
+
+impl ClipboardItem {
+ pub fn new(text: String) -> Self {
+ Self {
+ text,
+ metadata: None,
+ }
+ }
+
+ pub fn with_metadata<T: Serialize>(mut self, metadata: T) -> Self {
+ self.metadata = Some(serde_json::to_string(&metadata).unwrap());
+ self
+ }
+
+ pub fn text(&self) -> &String {
+ &self.text
+ }
+
+ pub fn metadata<T>(&self) -> Option<T>
+ where
+ T: for<'a> Deserialize<'a>,
+ {
+ self.metadata
+ .as_ref()
+ .and_then(|m| serde_json::from_str(m).ok())
+ }
+
+ pub(crate) fn text_hash(text: &str) -> u64 {
+ let mut hasher = SeaHasher::new();
+ text.hash(&mut hasher);
+ hasher.finish()
+ }
+}
@@ -0,0 +1,204 @@
+use crate::{point, Keystroke, Modifiers, Pixels, Point};
+use std::{any::Any, ops::Deref};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KeyDownEvent {
+ pub keystroke: Keystroke,
+ pub is_held: bool,
+}
+
+#[derive(Clone, Debug)]
+pub struct KeyUpEvent {
+ pub keystroke: Keystroke,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ModifiersChangedEvent {
+ pub modifiers: Modifiers,
+}
+
+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)]
+pub enum TouchPhase {
+ Started,
+ Moved,
+ Ended,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum ScrollDelta {
+ Pixels(Point<Pixels>),
+ Lines(Point<f32>),
+}
+
+impl Default for ScrollDelta {
+ fn default() -> Self {
+ Self::Lines(Default::default())
+ }
+}
+
+impl ScrollDelta {
+ pub fn precise(&self) -> bool {
+ match self {
+ ScrollDelta::Pixels(_) => true,
+ ScrollDelta::Lines(_) => false,
+ }
+ }
+
+ pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
+ match self {
+ ScrollDelta::Pixels(delta) => *delta,
+ ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ScrollWheelEvent {
+ pub position: Point<Pixels>,
+ pub delta: ScrollDelta,
+ pub modifiers: Modifiers,
+ /// If the platform supports returning the phase of a scroll wheel event, it will be stored here
+ pub phase: Option<TouchPhase>,
+}
+
+impl Deref for ScrollWheelEvent {
+ type Target = Modifiers;
+
+ fn deref(&self) -> &Self::Target {
+ &self.modifiers
+ }
+}
+
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
+pub enum NavigationDirection {
+ Back,
+ Forward,
+}
+
+impl Default for NavigationDirection {
+ fn default() -> Self {
+ Self::Back
+ }
+}
+
+#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
+pub enum MouseButton {
+ Left,
+ Right,
+ Middle,
+ Navigate(NavigationDirection),
+}
+
+impl MouseButton {
+ pub fn all() -> Vec<Self> {
+ vec![
+ MouseButton::Left,
+ MouseButton::Right,
+ MouseButton::Middle,
+ MouseButton::Navigate(NavigationDirection::Back),
+ MouseButton::Navigate(NavigationDirection::Forward),
+ ]
+ }
+}
+
+impl Default for MouseButton {
+ fn default() -> Self {
+ Self::Left
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseDownEvent {
+ pub button: MouseButton,
+ pub position: Point<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseUpEvent {
+ pub button: MouseButton,
+ pub position: Point<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseUp {
+ pub button: MouseButton,
+ pub position: Point<Pixels>,
+ pub modifiers: Modifiers,
+ pub click_count: usize,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseMovedEvent {
+ pub position: Point<Pixels>,
+ pub pressed_button: Option<MouseButton>,
+ pub modifiers: Modifiers,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct MouseExitedEvent {
+ pub position: Point<Pixels>,
+ pub pressed_button: Option<MouseButton>,
+ pub modifiers: Modifiers,
+}
+
+impl Deref for MouseExitedEvent {
+ type Target = Modifiers;
+
+ fn deref(&self) -> &Self::Target {
+ &self.modifiers
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum Event {
+ KeyDown(KeyDownEvent),
+ KeyUp(KeyUpEvent),
+ ModifiersChanged(ModifiersChangedEvent),
+ MouseDown(MouseDownEvent),
+ MouseUp(MouseUpEvent),
+ MouseMoved(MouseMovedEvent),
+ MouseExited(MouseExitedEvent),
+ ScrollWheel(ScrollWheelEvent),
+}
+
+impl Event {
+ pub fn position(&self) -> Option<Point<Pixels>> {
+ match self {
+ Event::KeyDown { .. } => None,
+ Event::KeyUp { .. } => None,
+ Event::ModifiersChanged { .. } => None,
+ Event::MouseDown(event) => Some(event.position),
+ Event::MouseUp(event) => Some(event.position),
+ Event::MouseMoved(event) => Some(event.position),
+ Event::MouseExited(event) => Some(event.position),
+ Event::ScrollWheel(event) => Some(event.position),
+ }
+ }
+
+ pub fn mouse_event<'a>(&'a self) -> Option<&'a dyn Any> {
+ match self {
+ Event::KeyDown { .. } => None,
+ Event::KeyUp { .. } => None,
+ Event::ModifiersChanged { .. } => None,
+ Event::MouseDown(event) => Some(event),
+ Event::MouseUp(event) => Some(event),
+ Event::MouseMoved(event) => Some(event),
+ Event::MouseExited(event) => Some(event),
+ Event::ScrollWheel(event) => Some(event),
+ }
+ }
+}
@@ -0,0 +1,121 @@
+use anyhow::anyhow;
+use serde::Deserialize;
+use std::fmt::Write;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
+pub struct Modifiers {
+ pub control: bool,
+ pub alt: bool,
+ pub shift: bool,
+ pub command: bool,
+ pub function: bool,
+}
+
+impl Modifiers {
+ pub fn modified(&self) -> bool {
+ self.control || self.alt || self.shift || self.command || self.function
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Hash)]
+pub struct Keystroke {
+ pub key: String,
+ pub modifiers: Modifiers,
+}
+
+impl Keystroke {
+ pub fn parse(source: &str) -> anyhow::Result<Self> {
+ let mut control = false;
+ let mut alt = false;
+ let mut shift = false;
+ let mut command = false;
+ let mut function = false;
+ let mut key = None;
+
+ let mut components = source.split('-').peekable();
+ while let Some(component) = components.next() {
+ match component {
+ "ctrl" => control = true,
+ "alt" => alt = true,
+ "shift" => shift = true,
+ "cmd" => command = true,
+ "fn" => function = true,
+ _ => {
+ if let Some(component) = components.peek() {
+ if component.is_empty() && source.ends_with('-') {
+ key = Some(String::from("-"));
+ break;
+ } else {
+ return Err(anyhow!("Invalid keystroke `{}`", source));
+ }
+ } else {
+ key = Some(String::from(component));
+ }
+ }
+ }
+ }
+
+ let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
+
+ Ok(Keystroke {
+ modifiers: Modifiers {
+ control,
+ alt,
+ shift,
+ command,
+ function,
+ },
+ key,
+ })
+ }
+
+ pub fn modified(&self) -> bool {
+ self.modifiers.modified()
+ }
+}
+
+impl std::fmt::Display for Keystroke {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Modifiers {
+ control,
+ alt,
+ shift,
+ command,
+ function,
+ } = self.modifiers;
+
+ if control {
+ f.write_char('^')?;
+ }
+ if alt {
+ f.write_char('⎇')?;
+ }
+ if command {
+ f.write_char('⌘')?;
+ }
+ if shift {
+ f.write_char('⇧')?;
+ }
+ if function {
+ f.write_char('𝙛')?;
+ }
+
+ let key = match self.key.as_str() {
+ "backspace" => '⌫',
+ "up" => '↑',
+ "down" => '↓',
+ "left" => '←',
+ "right" => '→',
+ "tab" => '⇥',
+ "escape" => '⎋',
+ key => {
+ if key.len() == 1 {
+ key.chars().next().unwrap().to_ascii_uppercase()
+ } else {
+ return f.write_str(key);
+ }
+ }
+ };
+ f.write_char(key)
+ }
+}
@@ -0,0 +1,151 @@
+///! Macos screen have a y axis that goings up from the bottom of the screen and
+///! an origin at the bottom left of the main display.
+mod dispatcher;
+mod events;
+mod metal_renderer;
+mod open_type;
+mod platform;
+mod screen;
+mod text_system;
+mod window;
+mod window_appearence;
+
+use crate::{px, size, Pixels, Size};
+use anyhow::anyhow;
+use cocoa::{
+ base::{id, nil},
+ foundation::{NSAutoreleasePool, NSNotFound, NSRect, NSSize, NSString, NSUInteger, NSURL},
+};
+use metal_renderer::*;
+use objc::{
+ msg_send,
+ runtime::{BOOL, NO, YES},
+ sel, sel_impl,
+};
+use std::{
+ ffi::{c_char, CStr, OsStr},
+ ops::Range,
+ os::unix::prelude::OsStrExt,
+ path::PathBuf,
+};
+
+pub use dispatcher::*;
+pub use platform::*;
+pub use screen::*;
+pub use text_system::*;
+pub use window::*;
+
+trait BoolExt {
+ fn to_objc(self) -> BOOL;
+}
+
+impl BoolExt for bool {
+ fn to_objc(self) -> BOOL {
+ if self {
+ YES
+ } else {
+ NO
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+struct NSRange {
+ pub location: NSUInteger,
+ pub length: NSUInteger,
+}
+
+impl NSRange {
+ fn invalid() -> Self {
+ Self {
+ location: NSNotFound as NSUInteger,
+ length: 0,
+ }
+ }
+
+ fn is_valid(&self) -> bool {
+ self.location != NSNotFound as NSUInteger
+ }
+
+ fn to_range(self) -> Option<Range<usize>> {
+ if self.is_valid() {
+ let start = self.location as usize;
+ let end = start + self.length as usize;
+ Some(start..end)
+ } else {
+ None
+ }
+ }
+}
+
+impl From<Range<usize>> for NSRange {
+ fn from(range: Range<usize>) -> Self {
+ NSRange {
+ location: range.start as NSUInteger,
+ length: range.len() as NSUInteger,
+ }
+ }
+}
+
+unsafe impl objc::Encode for NSRange {
+ fn encode() -> objc::Encoding {
+ let encoding = format!(
+ "{{NSRange={}{}}}",
+ NSUInteger::encode().as_str(),
+ NSUInteger::encode().as_str()
+ );
+ unsafe { objc::Encoding::from_str(&encoding) }
+ }
+}
+
+unsafe fn ns_string(string: &str) -> id {
+ NSString::alloc(nil).init_str(string).autorelease()
+}
+
+impl From<NSSize> for Size<Pixels> {
+ fn from(value: NSSize) -> Self {
+ Size {
+ width: px(value.width as f32),
+ height: px(value.height as f32),
+ }
+ }
+}
+
+pub trait NSRectExt {
+ fn size(&self) -> Size<Pixels>;
+ fn intersects(&self, other: Self) -> bool;
+}
+
+impl NSRectExt for NSRect {
+ fn size(&self) -> Size<Pixels> {
+ size(px(self.size.width as f32), px(self.size.height as f32))
+ }
+
+ fn intersects(&self, other: Self) -> bool {
+ self.size.width > 0.
+ && self.size.height > 0.
+ && other.size.width > 0.
+ && other.size.height > 0.
+ && self.origin.x <= other.origin.x + other.size.width
+ && self.origin.x + self.size.width >= other.origin.x
+ && self.origin.y <= other.origin.y + other.size.height
+ && self.origin.y + self.size.height >= other.origin.y
+ }
+}
+
+// todo!
+#[allow(unused)]
+unsafe fn ns_url_to_path(url: id) -> crate::Result<PathBuf> {
+ let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+ if path.is_null() {
+ Err(anyhow!(
+ "url is not a file path: {}",
+ CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+ ))
+ } else {
+ Ok(PathBuf::from(OsStr::from_bytes(
+ CStr::from_ptr(path).to_bytes(),
+ )))
+ }
+}
@@ -0,0 +1 @@
+#include <dispatch/dispatch.h>
@@ -0,0 +1,42 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+use crate::PlatformDispatcher;
+use async_task::Runnable;
+use objc::{
+ class, msg_send,
+ runtime::{BOOL, YES},
+ sel, sel_impl,
+};
+use std::ffi::c_void;
+
+include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
+
+pub fn dispatch_get_main_queue() -> dispatch_queue_t {
+ unsafe { &_dispatch_main_q as *const _ as dispatch_queue_t }
+}
+
+pub struct MacDispatcher;
+
+impl PlatformDispatcher for MacDispatcher {
+ fn is_main_thread(&self) -> bool {
+ let is_main_thread: BOOL = unsafe { msg_send![class!(NSThread), isMainThread] };
+ is_main_thread == YES
+ }
+
+ fn run_on_main_thread(&self, runnable: Runnable) {
+ unsafe {
+ dispatch_async_f(
+ dispatch_get_main_queue(),
+ runnable.into_raw() as *mut c_void,
+ Some(trampoline),
+ );
+ }
+
+ extern "C" fn trampoline(runnable: *mut c_void) {
+ let task = unsafe { Runnable::from_raw(runnable as *mut ()) };
+ task.run();
+ }
+ }
+}
@@ -0,0 +1,356 @@
+use crate::{
+ point, px, Event, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent,
+ MouseButton, MouseDownEvent, MouseExitedEvent, MouseMovedEvent, MouseUpEvent,
+ NavigationDirection, Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase,
+};
+use cocoa::{
+ appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
+ base::{id, YES},
+ foundation::NSString as _,
+};
+use core_graphics::{
+ event::{CGEvent, CGEventFlags, CGKeyCode},
+ event_source::{CGEventSource, CGEventSourceStateID},
+};
+use ctor::ctor;
+use foreign_types::ForeignType;
+use objc::{class, msg_send, sel, sel_impl};
+use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
+
+const BACKSPACE_KEY: u16 = 0x7f;
+const SPACE_KEY: u16 = b' ' as u16;
+const ENTER_KEY: u16 = 0x0d;
+const NUMPAD_ENTER_KEY: u16 = 0x03;
+const ESCAPE_KEY: u16 = 0x1b;
+const TAB_KEY: u16 = 0x09;
+const SHIFT_TAB_KEY: u16 = 0x19;
+
+static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
+
+#[ctor]
+unsafe fn build_event_source() {
+ let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
+ EVENT_SOURCE = source.as_ptr();
+ mem::forget(source);
+}
+
+// todo!
+#[allow(unused)]
+pub fn key_to_native(key: &str) -> Cow<str> {
+ use cocoa::appkit::*;
+ let code = match key {
+ "space" => SPACE_KEY,
+ "backspace" => BACKSPACE_KEY,
+ "up" => NSUpArrowFunctionKey,
+ "down" => NSDownArrowFunctionKey,
+ "left" => NSLeftArrowFunctionKey,
+ "right" => NSRightArrowFunctionKey,
+ "pageup" => NSPageUpFunctionKey,
+ "pagedown" => NSPageDownFunctionKey,
+ "home" => NSHomeFunctionKey,
+ "end" => NSEndFunctionKey,
+ "delete" => NSDeleteFunctionKey,
+ "f1" => NSF1FunctionKey,
+ "f2" => NSF2FunctionKey,
+ "f3" => NSF3FunctionKey,
+ "f4" => NSF4FunctionKey,
+ "f5" => NSF5FunctionKey,
+ "f6" => NSF6FunctionKey,
+ "f7" => NSF7FunctionKey,
+ "f8" => NSF8FunctionKey,
+ "f9" => NSF9FunctionKey,
+ "f10" => NSF10FunctionKey,
+ "f11" => NSF11FunctionKey,
+ "f12" => NSF12FunctionKey,
+ _ => return Cow::Borrowed(key),
+ };
+ Cow::Owned(String::from_utf16(&[code]).unwrap())
+}
+
+unsafe fn read_modifiers(native_event: id) -> Modifiers {
+ let modifiers = native_event.modifierFlags();
+ let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+ let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+ let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+ let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+ let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
+
+ Modifiers {
+ control,
+ alt,
+ shift,
+ command,
+ function,
+ }
+}
+
+impl Event {
+ pub unsafe fn from_native(native_event: id, window_height: Option<Pixels>) -> Option<Self> {
+ let event_type = native_event.eventType();
+
+ // Filter out event types that aren't in the NSEventType enum.
+ // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
+ match event_type as u64 {
+ 0 | 21 | 32 | 33 | 35 | 36 | 37 => {
+ return None;
+ }
+ _ => {}
+ }
+
+ match event_type {
+ NSEventType::NSFlagsChanged => Some(Self::ModifiersChanged(ModifiersChangedEvent {
+ modifiers: read_modifiers(native_event),
+ })),
+ NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
+ keystroke: parse_keystroke(native_event),
+ is_held: native_event.isARepeat() == YES,
+ })),
+ NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
+ keystroke: parse_keystroke(native_event),
+ })),
+ NSEventType::NSLeftMouseDown
+ | NSEventType::NSRightMouseDown
+ | NSEventType::NSOtherMouseDown => {
+ let button = match native_event.buttonNumber() {
+ 0 => MouseButton::Left,
+ 1 => MouseButton::Right,
+ 2 => MouseButton::Middle,
+ 3 => MouseButton::Navigate(NavigationDirection::Back),
+ 4 => MouseButton::Navigate(NavigationDirection::Forward),
+ // Other mouse buttons aren't tracked currently
+ _ => return None,
+ };
+ window_height.map(|window_height| {
+ Self::MouseDown(MouseDownEvent {
+ button,
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ // MacOS screen coordinates are relative to bottom left
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ modifiers: read_modifiers(native_event),
+ click_count: native_event.clickCount() as usize,
+ })
+ })
+ }
+ NSEventType::NSLeftMouseUp
+ | NSEventType::NSRightMouseUp
+ | NSEventType::NSOtherMouseUp => {
+ let button = match native_event.buttonNumber() {
+ 0 => MouseButton::Left,
+ 1 => MouseButton::Right,
+ 2 => MouseButton::Middle,
+ 3 => MouseButton::Navigate(NavigationDirection::Back),
+ 4 => MouseButton::Navigate(NavigationDirection::Forward),
+ // Other mouse buttons aren't tracked currently
+ _ => return None,
+ };
+
+ window_height.map(|window_height| {
+ Self::MouseUp(MouseUpEvent {
+ button,
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ modifiers: read_modifiers(native_event),
+ click_count: native_event.clickCount() as usize,
+ })
+ })
+ }
+ NSEventType::NSScrollWheel => window_height.map(|window_height| {
+ let phase = match native_event.phase() {
+ NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
+ Some(TouchPhase::Started)
+ }
+ NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended),
+ _ => Some(TouchPhase::Moved),
+ };
+
+ let raw_data = point(
+ native_event.scrollingDeltaX() as f32,
+ native_event.scrollingDeltaY() as f32,
+ );
+
+ let delta = if native_event.hasPreciseScrollingDeltas() == YES {
+ ScrollDelta::Pixels(raw_data.map(px))
+ } else {
+ ScrollDelta::Lines(raw_data)
+ };
+
+ Self::ScrollWheel(ScrollWheelEvent {
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ delta,
+ phase,
+ modifiers: read_modifiers(native_event),
+ })
+ }),
+ NSEventType::NSLeftMouseDragged
+ | NSEventType::NSRightMouseDragged
+ | NSEventType::NSOtherMouseDragged => {
+ let pressed_button = match native_event.buttonNumber() {
+ 0 => MouseButton::Left,
+ 1 => MouseButton::Right,
+ 2 => MouseButton::Middle,
+ 3 => MouseButton::Navigate(NavigationDirection::Back),
+ 4 => MouseButton::Navigate(NavigationDirection::Forward),
+ // Other mouse buttons aren't tracked currently
+ _ => return None,
+ };
+
+ window_height.map(|window_height| {
+ Self::MouseMoved(MouseMovedEvent {
+ pressed_button: Some(pressed_button),
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ modifiers: read_modifiers(native_event),
+ })
+ })
+ }
+ NSEventType::NSMouseMoved => window_height.map(|window_height| {
+ Self::MouseMoved(MouseMovedEvent {
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+ pressed_button: None,
+ modifiers: read_modifiers(native_event),
+ })
+ }),
+ NSEventType::NSMouseExited => window_height.map(|window_height| {
+ Self::MouseExited(MouseExitedEvent {
+ position: point(
+ px(native_event.locationInWindow().x as f32),
+ window_height - px(native_event.locationInWindow().y as f32),
+ ),
+
+ pressed_button: None,
+ modifiers: read_modifiers(native_event),
+ })
+ }),
+ _ => None,
+ }
+ }
+}
+
+unsafe fn parse_keystroke(native_event: id) -> Keystroke {
+ use cocoa::appkit::*;
+
+ let mut chars_ignoring_modifiers =
+ CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
+ .to_str()
+ .unwrap()
+ .to_string();
+ let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
+ let modifiers = native_event.modifierFlags();
+
+ let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+ let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+ let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+ let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+ let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
+ && first_char.map_or(true, |ch| {
+ !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
+ });
+
+ #[allow(non_upper_case_globals)]
+ let key = match first_char {
+ Some(SPACE_KEY) => "space".to_string(),
+ Some(BACKSPACE_KEY) => "backspace".to_string(),
+ Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
+ Some(ESCAPE_KEY) => "escape".to_string(),
+ Some(TAB_KEY) => "tab".to_string(),
+ Some(SHIFT_TAB_KEY) => "tab".to_string(),
+ Some(NSUpArrowFunctionKey) => "up".to_string(),
+ Some(NSDownArrowFunctionKey) => "down".to_string(),
+ Some(NSLeftArrowFunctionKey) => "left".to_string(),
+ Some(NSRightArrowFunctionKey) => "right".to_string(),
+ Some(NSPageUpFunctionKey) => "pageup".to_string(),
+ Some(NSPageDownFunctionKey) => "pagedown".to_string(),
+ Some(NSHomeFunctionKey) => "home".to_string(),
+ Some(NSEndFunctionKey) => "end".to_string(),
+ Some(NSDeleteFunctionKey) => "delete".to_string(),
+ Some(NSF1FunctionKey) => "f1".to_string(),
+ Some(NSF2FunctionKey) => "f2".to_string(),
+ Some(NSF3FunctionKey) => "f3".to_string(),
+ Some(NSF4FunctionKey) => "f4".to_string(),
+ Some(NSF5FunctionKey) => "f5".to_string(),
+ Some(NSF6FunctionKey) => "f6".to_string(),
+ Some(NSF7FunctionKey) => "f7".to_string(),
+ Some(NSF8FunctionKey) => "f8".to_string(),
+ Some(NSF9FunctionKey) => "f9".to_string(),
+ Some(NSF10FunctionKey) => "f10".to_string(),
+ Some(NSF11FunctionKey) => "f11".to_string(),
+ Some(NSF12FunctionKey) => "f12".to_string(),
+ _ => {
+ let mut chars_ignoring_modifiers_and_shift =
+ chars_for_modified_key(native_event.keyCode(), false, false);
+
+ // Honor ⌘ when Dvorak-QWERTY is used.
+ let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
+ if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
+ chars_ignoring_modifiers =
+ chars_for_modified_key(native_event.keyCode(), true, shift);
+ chars_ignoring_modifiers_and_shift = chars_with_cmd;
+ }
+
+ if shift {
+ if chars_ignoring_modifiers_and_shift
+ == chars_ignoring_modifiers.to_ascii_lowercase()
+ {
+ chars_ignoring_modifiers_and_shift
+ } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
+ shift = false;
+ chars_ignoring_modifiers
+ } else {
+ chars_ignoring_modifiers
+ }
+ } else {
+ chars_ignoring_modifiers
+ }
+ }
+ };
+
+ Keystroke {
+ modifiers: Modifiers {
+ control,
+ alt,
+ shift,
+ command,
+ function,
+ },
+ key,
+ }
+}
+
+fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
+ // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
+ // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
+ // an event with the given flags instead lets us access `characters`, which always
+ // returns a valid string.
+ let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
+ let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
+ mem::forget(source);
+
+ let mut flags = CGEventFlags::empty();
+ if cmd {
+ flags |= CGEventFlags::CGEventFlagCommand;
+ }
+ if shift {
+ flags |= CGEventFlags::CGEventFlagShift;
+ }
+ event.set_flags(flags);
+
+ unsafe {
+ let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
+ CStr::from_ptr(event.characters().UTF8String())
+ .to_str()
+ .unwrap()
+ .to_string()
+ }
+}
@@ -0,0 +1,294 @@
+use crate::{point, size, DevicePixels, Quad, Scene, Size};
+use bytemuck::{Pod, Zeroable};
+use cocoa::{
+ base::{NO, YES},
+ foundation::NSUInteger,
+ quartzcore::AutoresizingMask,
+};
+use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
+use objc::{self, msg_send, sel, sel_impl};
+use std::{ffi::c_void, mem, ptr};
+
+const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
+const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
+
+pub struct MetalRenderer {
+ device: metal::Device,
+ layer: metal::MetalLayer,
+ command_queue: CommandQueue,
+ quad_pipeline_state: metal::RenderPipelineState,
+ unit_vertices: metal::Buffer,
+ instances: metal::Buffer,
+}
+
+impl MetalRenderer {
+ pub fn new(is_opaque: bool) -> Self {
+ const PIXEL_FORMAT: MTLPixelFormat = MTLPixelFormat::BGRA8Unorm;
+
+ let device: metal::Device = if let Some(device) = metal::Device::system_default() {
+ device
+ } else {
+ log::error!("unable to access a compatible graphics device");
+ std::process::exit(1);
+ };
+
+ let layer = metal::MetalLayer::new();
+ layer.set_device(&device);
+ layer.set_pixel_format(PIXEL_FORMAT);
+ layer.set_presents_with_transaction(true);
+ layer.set_opaque(is_opaque);
+ unsafe {
+ let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
+ let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
+ let _: () = msg_send![
+ &*layer,
+ setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
+ | AutoresizingMask::HEIGHT_SIZABLE
+ ];
+ }
+
+ let library = device
+ .new_library_with_data(SHADERS_METALLIB)
+ .expect("error building metal library");
+
+ fn to_float2_bits(point: crate::PointF) -> u64 {
+ unsafe {
+ let mut output = mem::transmute::<_, u32>(point.y.to_bits()) as u64;
+ output <<= 32;
+ output |= mem::transmute::<_, u32>(point.x.to_bits()) as u64;
+ output
+ }
+ }
+
+ let unit_vertices = [
+ to_float2_bits(point(0., 0.)),
+ to_float2_bits(point(1., 0.)),
+ to_float2_bits(point(0., 1.)),
+ to_float2_bits(point(0., 1.)),
+ to_float2_bits(point(1., 0.)),
+ to_float2_bits(point(1., 1.)),
+ ];
+ let unit_vertices = device.new_buffer_with_data(
+ unit_vertices.as_ptr() as *const c_void,
+ (unit_vertices.len() * mem::size_of::<u64>()) as u64,
+ MTLResourceOptions::StorageModeManaged,
+ );
+ let instances = device.new_buffer(
+ INSTANCE_BUFFER_SIZE as u64,
+ MTLResourceOptions::StorageModeManaged,
+ );
+
+ let quad_pipeline_state = build_pipeline_state(
+ &device,
+ &library,
+ "quad",
+ "quad_vertex",
+ "quad_fragment",
+ PIXEL_FORMAT,
+ );
+
+ let command_queue = device.new_command_queue();
+ Self {
+ device,
+ layer,
+ command_queue,
+ quad_pipeline_state,
+ unit_vertices,
+ instances,
+ }
+ }
+
+ pub fn layer(&self) -> &metal::MetalLayerRef {
+ &*self.layer
+ }
+
+ pub fn draw(&mut self, scene: &Scene) {
+ dbg!(scene);
+
+ let layer = self.layer.clone();
+ let viewport_size = layer.drawable_size();
+ let viewport_size: Size<DevicePixels> = size(
+ (viewport_size.width.ceil() as u32).into(),
+ (viewport_size.height.ceil() as u32).into(),
+ );
+ let drawable = if let Some(drawable) = layer.next_drawable() {
+ drawable
+ } else {
+ log::error!(
+ "failed to retrieve next drawable, drawable size: {:?}",
+ viewport_size
+ );
+ return;
+ };
+ let command_queue = self.command_queue.clone();
+ let command_buffer = command_queue.new_command_buffer();
+
+ let render_pass_descriptor = metal::RenderPassDescriptor::new();
+
+ let depth_texture_desc = metal::TextureDescriptor::new();
+ depth_texture_desc.set_pixel_format(metal::MTLPixelFormat::Depth32Float);
+ depth_texture_desc.set_storage_mode(metal::MTLStorageMode::Private);
+ depth_texture_desc.set_usage(metal::MTLTextureUsage::RenderTarget);
+ depth_texture_desc.set_width(u32::from(viewport_size.width) as u64);
+ depth_texture_desc.set_height(u32::from(viewport_size.height) as u64);
+ let depth_texture = self.device.new_texture(&depth_texture_desc);
+ let depth_attachment = render_pass_descriptor.depth_attachment().unwrap();
+
+ depth_attachment.set_texture(Some(&depth_texture));
+ depth_attachment.set_clear_depth(1.);
+ depth_attachment.set_store_action(metal::MTLStoreAction::Store);
+
+ let color_attachment = render_pass_descriptor
+ .color_attachments()
+ .object_at(0)
+ .unwrap();
+
+ color_attachment.set_texture(Some(drawable.texture()));
+ color_attachment.set_load_action(metal::MTLLoadAction::Clear);
+ color_attachment.set_store_action(metal::MTLStoreAction::Store);
+ let alpha = if self.layer.is_opaque() { 1. } else { 0. };
+ color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
+ let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
+
+ command_encoder.set_viewport(metal::MTLViewport {
+ originX: 0.0,
+ originY: 0.0,
+ width: u32::from(viewport_size.width) as f64,
+ height: u32::from(viewport_size.height) as f64,
+ znear: 0.0,
+ zfar: 1.0,
+ });
+
+ let mut buffer_offset = 0;
+ for layer in scene.layers() {
+ self.draw_quads(
+ &layer.quads,
+ &mut buffer_offset,
+ viewport_size,
+ command_encoder,
+ );
+ }
+
+ command_encoder.end_encoding();
+
+ self.instances.did_modify_range(NSRange {
+ location: 0,
+ length: buffer_offset as NSUInteger,
+ });
+
+ command_buffer.commit();
+ command_buffer.wait_until_completed();
+ drawable.present();
+ }
+
+ fn draw_quads(
+ &mut self,
+ quads: &[Quad],
+ offset: &mut usize,
+ viewport_size: Size<DevicePixels>,
+ command_encoder: &metal::RenderCommandEncoderRef,
+ ) {
+ if quads.is_empty() {
+ return;
+ }
+ align_offset(offset);
+
+ command_encoder.set_render_pipeline_state(&self.quad_pipeline_state);
+ command_encoder.set_vertex_buffer(
+ QuadInputIndex::Vertices as u64,
+ Some(&self.unit_vertices),
+ 0,
+ );
+ command_encoder.set_vertex_buffer(
+ QuadInputIndex::Quads as u64,
+ Some(&self.instances),
+ *offset as u64,
+ );
+ command_encoder.set_fragment_buffer(
+ QuadInputIndex::Quads as u64,
+ Some(&self.instances),
+ *offset as u64,
+ );
+ let quad_uniforms = QuadUniforms { viewport_size };
+
+ let quad_uniform_bytes = bytemuck::bytes_of(&quad_uniforms);
+ command_encoder.set_vertex_bytes(
+ QuadInputIndex::Uniforms as u64,
+ quad_uniform_bytes.len() as u64,
+ quad_uniform_bytes.as_ptr() as *const c_void,
+ );
+
+ let quad_bytes = bytemuck::cast_slice(quads);
+ let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+ unsafe {
+ ptr::copy_nonoverlapping(quad_bytes.as_ptr(), buffer_contents, quad_bytes.len());
+ }
+
+ let next_offset = *offset + quad_bytes.len();
+ assert!(
+ next_offset <= INSTANCE_BUFFER_SIZE,
+ "instance buffer exhausted"
+ );
+
+ command_encoder.draw_primitives_instanced(
+ metal::MTLPrimitiveType::Triangle,
+ 0,
+ 6,
+ quads.len() as u64,
+ );
+ *offset = next_offset;
+ }
+}
+
+fn build_pipeline_state(
+ device: &metal::DeviceRef,
+ library: &metal::LibraryRef,
+ label: &str,
+ vertex_fn_name: &str,
+ fragment_fn_name: &str,
+ pixel_format: metal::MTLPixelFormat,
+) -> metal::RenderPipelineState {
+ let vertex_fn = library
+ .get_function(vertex_fn_name, None)
+ .expect("error locating vertex function");
+ let fragment_fn = library
+ .get_function(fragment_fn_name, None)
+ .expect("error locating fragment function");
+
+ let descriptor = metal::RenderPipelineDescriptor::new();
+ descriptor.set_label(label);
+ descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
+ descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
+ let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
+ color_attachment.set_pixel_format(pixel_format);
+ color_attachment.set_blending_enabled(true);
+ color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add);
+ color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add);
+ color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::SourceAlpha);
+ color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One);
+ color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha);
+ color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One);
+ // descriptor.set_depth_attachment_pixel_format(MTLPixelFormat::Depth32Float);
+
+ device
+ .new_render_pipeline_state(&descriptor)
+ .expect("could not create render pipeline state")
+}
+
+// Align to multiples of 256 make Metal happy.
+fn align_offset(offset: &mut usize) {
+ *offset = ((*offset + 255) / 256) * 256;
+}
+
+#[repr(C)]
+enum QuadInputIndex {
+ Vertices = 0,
+ Quads = 1,
+ Uniforms = 2,
+}
+
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
+#[repr(C)]
+pub(crate) struct QuadUniforms {
+ viewport_size: Size<DevicePixels>,
+}
@@ -0,0 +1,394 @@
+#![allow(unused, non_upper_case_globals)]
+
+use crate::FontFeatures;
+use cocoa::appkit::CGFloat;
+use core_foundation::{base::TCFType, number::CFNumber};
+use core_graphics::geometry::CGAffineTransform;
+use core_text::{
+ font::{CTFont, CTFontRef},
+ font_descriptor::{
+ CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
+ },
+};
+use font_kit::font::Font;
+use std::ptr;
+
+const kCaseSensitiveLayoutOffSelector: i32 = 1;
+const kCaseSensitiveLayoutOnSelector: i32 = 0;
+const kCaseSensitiveLayoutType: i32 = 33;
+const kCaseSensitiveSpacingOffSelector: i32 = 3;
+const kCaseSensitiveSpacingOnSelector: i32 = 2;
+const kCharacterAlternativesType: i32 = 17;
+const kCommonLigaturesOffSelector: i32 = 3;
+const kCommonLigaturesOnSelector: i32 = 2;
+const kContextualAlternatesOffSelector: i32 = 1;
+const kContextualAlternatesOnSelector: i32 = 0;
+const kContextualAlternatesType: i32 = 36;
+const kContextualLigaturesOffSelector: i32 = 19;
+const kContextualLigaturesOnSelector: i32 = 18;
+const kContextualSwashAlternatesOffSelector: i32 = 5;
+const kContextualSwashAlternatesOnSelector: i32 = 4;
+const kDefaultLowerCaseSelector: i32 = 0;
+const kDefaultUpperCaseSelector: i32 = 0;
+const kDiagonalFractionsSelector: i32 = 2;
+const kFractionsType: i32 = 11;
+const kHistoricalLigaturesOffSelector: i32 = 21;
+const kHistoricalLigaturesOnSelector: i32 = 20;
+const kHojoCharactersSelector: i32 = 12;
+const kInferiorsSelector: i32 = 2;
+const kJIS2004CharactersSelector: i32 = 11;
+const kLigaturesType: i32 = 1;
+const kLowerCasePetiteCapsSelector: i32 = 2;
+const kLowerCaseSmallCapsSelector: i32 = 1;
+const kLowerCaseType: i32 = 37;
+const kLowerCaseNumbersSelector: i32 = 0;
+const kMathematicalGreekOffSelector: i32 = 11;
+const kMathematicalGreekOnSelector: i32 = 10;
+const kMonospacedNumbersSelector: i32 = 0;
+const kNLCCharactersSelector: i32 = 13;
+const kNoFractionsSelector: i32 = 0;
+const kNormalPositionSelector: i32 = 0;
+const kNoStyleOptionsSelector: i32 = 0;
+const kNumberCaseType: i32 = 21;
+const kNumberSpacingType: i32 = 6;
+const kOrdinalsSelector: i32 = 3;
+const kProportionalNumbersSelector: i32 = 1;
+const kQuarterWidthTextSelector: i32 = 4;
+const kScientificInferiorsSelector: i32 = 4;
+const kSlashedZeroOffSelector: i32 = 5;
+const kSlashedZeroOnSelector: i32 = 4;
+const kStyleOptionsType: i32 = 19;
+const kStylisticAltEighteenOffSelector: i32 = 37;
+const kStylisticAltEighteenOnSelector: i32 = 36;
+const kStylisticAltEightOffSelector: i32 = 17;
+const kStylisticAltEightOnSelector: i32 = 16;
+const kStylisticAltElevenOffSelector: i32 = 23;
+const kStylisticAltElevenOnSelector: i32 = 22;
+const kStylisticAlternativesType: i32 = 35;
+const kStylisticAltFifteenOffSelector: i32 = 31;
+const kStylisticAltFifteenOnSelector: i32 = 30;
+const kStylisticAltFiveOffSelector: i32 = 11;
+const kStylisticAltFiveOnSelector: i32 = 10;
+const kStylisticAltFourOffSelector: i32 = 9;
+const kStylisticAltFourOnSelector: i32 = 8;
+const kStylisticAltFourteenOffSelector: i32 = 29;
+const kStylisticAltFourteenOnSelector: i32 = 28;
+const kStylisticAltNineOffSelector: i32 = 19;
+const kStylisticAltNineOnSelector: i32 = 18;
+const kStylisticAltNineteenOffSelector: i32 = 39;
+const kStylisticAltNineteenOnSelector: i32 = 38;
+const kStylisticAltOneOffSelector: i32 = 3;
+const kStylisticAltOneOnSelector: i32 = 2;
+const kStylisticAltSevenOffSelector: i32 = 15;
+const kStylisticAltSevenOnSelector: i32 = 14;
+const kStylisticAltSeventeenOffSelector: i32 = 35;
+const kStylisticAltSeventeenOnSelector: i32 = 34;
+const kStylisticAltSixOffSelector: i32 = 13;
+const kStylisticAltSixOnSelector: i32 = 12;
+const kStylisticAltSixteenOffSelector: i32 = 33;
+const kStylisticAltSixteenOnSelector: i32 = 32;
+const kStylisticAltTenOffSelector: i32 = 21;
+const kStylisticAltTenOnSelector: i32 = 20;
+const kStylisticAltThirteenOffSelector: i32 = 27;
+const kStylisticAltThirteenOnSelector: i32 = 26;
+const kStylisticAltThreeOffSelector: i32 = 7;
+const kStylisticAltThreeOnSelector: i32 = 6;
+const kStylisticAltTwelveOffSelector: i32 = 25;
+const kStylisticAltTwelveOnSelector: i32 = 24;
+const kStylisticAltTwentyOffSelector: i32 = 41;
+const kStylisticAltTwentyOnSelector: i32 = 40;
+const kStylisticAltTwoOffSelector: i32 = 5;
+const kStylisticAltTwoOnSelector: i32 = 4;
+const kSuperiorsSelector: i32 = 1;
+const kSwashAlternatesOffSelector: i32 = 3;
+const kSwashAlternatesOnSelector: i32 = 2;
+const kTitlingCapsSelector: i32 = 4;
+const kTypographicExtrasType: i32 = 14;
+const kVerticalFractionsSelector: i32 = 1;
+const kVerticalPositionType: i32 = 10;
+
+pub fn apply_features(font: &mut Font, features: FontFeatures) {
+ // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
+ // for a reference implementation.
+ toggle_open_type_feature(
+ font,
+ features.calt(),
+ kContextualAlternatesType,
+ kContextualAlternatesOnSelector,
+ kContextualAlternatesOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.case(),
+ kCaseSensitiveLayoutType,
+ kCaseSensitiveLayoutOnSelector,
+ kCaseSensitiveLayoutOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.cpsp(),
+ kCaseSensitiveLayoutType,
+ kCaseSensitiveSpacingOnSelector,
+ kCaseSensitiveSpacingOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.frac(),
+ kFractionsType,
+ kDiagonalFractionsSelector,
+ kNoFractionsSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.liga(),
+ kLigaturesType,
+ kCommonLigaturesOnSelector,
+ kCommonLigaturesOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.onum(),
+ kNumberCaseType,
+ kLowerCaseNumbersSelector,
+ 2,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ordn(),
+ kVerticalPositionType,
+ kOrdinalsSelector,
+ kNormalPositionSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.pnum(),
+ kNumberSpacingType,
+ kProportionalNumbersSelector,
+ 4,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss01(),
+ kStylisticAlternativesType,
+ kStylisticAltOneOnSelector,
+ kStylisticAltOneOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss02(),
+ kStylisticAlternativesType,
+ kStylisticAltTwoOnSelector,
+ kStylisticAltTwoOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss03(),
+ kStylisticAlternativesType,
+ kStylisticAltThreeOnSelector,
+ kStylisticAltThreeOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss04(),
+ kStylisticAlternativesType,
+ kStylisticAltFourOnSelector,
+ kStylisticAltFourOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss05(),
+ kStylisticAlternativesType,
+ kStylisticAltFiveOnSelector,
+ kStylisticAltFiveOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss06(),
+ kStylisticAlternativesType,
+ kStylisticAltSixOnSelector,
+ kStylisticAltSixOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss07(),
+ kStylisticAlternativesType,
+ kStylisticAltSevenOnSelector,
+ kStylisticAltSevenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss08(),
+ kStylisticAlternativesType,
+ kStylisticAltEightOnSelector,
+ kStylisticAltEightOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss09(),
+ kStylisticAlternativesType,
+ kStylisticAltNineOnSelector,
+ kStylisticAltNineOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss10(),
+ kStylisticAlternativesType,
+ kStylisticAltTenOnSelector,
+ kStylisticAltTenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss11(),
+ kStylisticAlternativesType,
+ kStylisticAltElevenOnSelector,
+ kStylisticAltElevenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss12(),
+ kStylisticAlternativesType,
+ kStylisticAltTwelveOnSelector,
+ kStylisticAltTwelveOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss13(),
+ kStylisticAlternativesType,
+ kStylisticAltThirteenOnSelector,
+ kStylisticAltThirteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss14(),
+ kStylisticAlternativesType,
+ kStylisticAltFourteenOnSelector,
+ kStylisticAltFourteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss15(),
+ kStylisticAlternativesType,
+ kStylisticAltFifteenOnSelector,
+ kStylisticAltFifteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss16(),
+ kStylisticAlternativesType,
+ kStylisticAltSixteenOnSelector,
+ kStylisticAltSixteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss17(),
+ kStylisticAlternativesType,
+ kStylisticAltSeventeenOnSelector,
+ kStylisticAltSeventeenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss18(),
+ kStylisticAlternativesType,
+ kStylisticAltEighteenOnSelector,
+ kStylisticAltEighteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss19(),
+ kStylisticAlternativesType,
+ kStylisticAltNineteenOnSelector,
+ kStylisticAltNineteenOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.ss20(),
+ kStylisticAlternativesType,
+ kStylisticAltTwentyOnSelector,
+ kStylisticAltTwentyOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.subs(),
+ kVerticalPositionType,
+ kInferiorsSelector,
+ kNormalPositionSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.sups(),
+ kVerticalPositionType,
+ kSuperiorsSelector,
+ kNormalPositionSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.swsh(),
+ kContextualAlternatesType,
+ kSwashAlternatesOnSelector,
+ kSwashAlternatesOffSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.titl(),
+ kStyleOptionsType,
+ kTitlingCapsSelector,
+ kNoStyleOptionsSelector,
+ );
+ toggle_open_type_feature(
+ font,
+ features.tnum(),
+ kNumberSpacingType,
+ kMonospacedNumbersSelector,
+ 4,
+ );
+ toggle_open_type_feature(
+ font,
+ features.zero(),
+ kTypographicExtrasType,
+ kSlashedZeroOnSelector,
+ kSlashedZeroOffSelector,
+ );
+}
+
+fn toggle_open_type_feature(
+ font: &mut Font,
+ enabled: Option<bool>,
+ type_identifier: i32,
+ on_selector_identifier: i32,
+ off_selector_identifier: i32,
+) {
+ if let Some(enabled) = enabled {
+ let native_font = font.native_font();
+ unsafe {
+ let selector_identifier = if enabled {
+ on_selector_identifier
+ } else {
+ off_selector_identifier
+ };
+ let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
+ native_font.copy_descriptor().as_concrete_TypeRef(),
+ CFNumber::from(type_identifier).as_concrete_TypeRef(),
+ CFNumber::from(selector_identifier).as_concrete_TypeRef(),
+ );
+ let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
+ let new_font = CTFontCreateCopyWithAttributes(
+ font.native_font().as_concrete_TypeRef(),
+ 0.0,
+ ptr::null(),
+ new_descriptor.as_concrete_TypeRef(),
+ );
+ let new_font = CTFont::wrap_under_create_rule(new_font);
+ *font = Font::from_native_font(new_font);
+ }
+ }
+}
+
+#[link(name = "CoreText", kind = "framework")]
+extern "C" {
+ fn CTFontCreateCopyWithAttributes(
+ font: CTFontRef,
+ size: CGFloat,
+ matrix: *const CGAffineTransform,
+ attributes: CTFontDescriptorRef,
+ ) -> CTFontRef;
+}
@@ -0,0 +1,1123 @@
+use super::BoolExt;
+use crate::{
+ AnyWindowHandle, ClipboardItem, CursorStyle, Event, MacDispatcher, MacScreen, MacTextSystem,
+ MacWindow, PathPromptOptions, Platform, PlatformScreen, PlatformTextSystem, PlatformWindow,
+ Result, ScreenId, SemanticVersion, WindowOptions,
+};
+use anyhow::anyhow;
+use block::ConcreteBlock;
+use cocoa::{
+ appkit::{
+ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
+ NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow,
+ },
+ base::{id, nil, BOOL, YES},
+ foundation::{
+ NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
+ NSUInteger, NSURL,
+ },
+};
+use core_foundation::{
+ base::{CFType, CFTypeRef, OSStatus, TCFType as _},
+ boolean::CFBoolean,
+ data::CFData,
+ dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
+ string::{CFString, CFStringRef},
+};
+use ctor::ctor;
+use futures::channel::oneshot;
+use objc::{
+ class,
+ declare::ClassDecl,
+ msg_send,
+ runtime::{Class, Object, Sel},
+ sel, sel_impl,
+};
+use parking_lot::Mutex;
+use ptr::null_mut;
+use std::{
+ cell::Cell,
+ convert::TryInto,
+ ffi::{c_void, CStr, OsStr},
+ os::{raw::c_char, unix::ffi::OsStrExt},
+ path::{Path, PathBuf},
+ process::Command,
+ ptr,
+ rc::Rc,
+ slice, str,
+ sync::Arc,
+};
+use time::UtcOffset;
+
+#[allow(non_upper_case_globals)]
+const NSUTF8StringEncoding: NSUInteger = 4;
+
+#[allow(non_upper_case_globals)]
+pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+
+const MAC_PLATFORM_IVAR: &str = "platform";
+static mut APP_CLASS: *const Class = ptr::null();
+static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
+
+#[ctor]
+unsafe fn build_classes() {
+ APP_CLASS = {
+ let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
+ decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
+ decl.add_method(
+ sel!(sendEvent:),
+ send_event as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.register()
+ };
+
+ APP_DELEGATE_CLASS = {
+ let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
+ decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
+ decl.add_method(
+ sel!(applicationDidFinishLaunching:),
+ did_finish_launching as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(applicationShouldHandleReopen:hasVisibleWindows:),
+ should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool),
+ );
+ decl.add_method(
+ sel!(applicationDidBecomeActive:),
+ did_become_active as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(applicationDidResignActive:),
+ did_resign_active as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(applicationWillTerminate:),
+ will_terminate as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(handleGPUIMenuItem:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ // Add menu item handlers so that OS save panels have the correct key commands
+ decl.add_method(
+ sel!(cut:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(copy:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(paste:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(selectAll:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(undo:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(redo:),
+ handle_menu_item as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(validateMenuItem:),
+ validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
+ );
+ decl.add_method(
+ sel!(menuWillOpen:),
+ menu_will_open as extern "C" fn(&mut Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(application:openURLs:),
+ open_urls as extern "C" fn(&mut Object, Sel, id, id),
+ );
+ decl.register()
+ }
+}
+
+pub struct MacPlatform(Mutex<MacPlatformState>);
+
+pub struct MacPlatformState {
+ dispatcher: Arc<MacDispatcher>,
+ text_system: Arc<MacTextSystem>,
+ pasteboard: id,
+ text_hash_pasteboard_type: id,
+ metadata_pasteboard_type: id,
+ become_active: Option<Box<dyn FnMut()>>,
+ resign_active: Option<Box<dyn FnMut()>>,
+ reopen: Option<Box<dyn FnMut()>>,
+ quit: Option<Box<dyn FnMut()>>,
+ event: Option<Box<dyn FnMut(Event) -> bool>>,
+ // menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+ // validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+ will_open_menu: Option<Box<dyn FnMut()>>,
+ open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
+ finish_launching: Option<Box<dyn FnOnce()>>,
+ // menu_actions: Vec<Box<dyn Action>>,
+}
+
+impl MacPlatform {
+ pub fn new() -> Self {
+ Self(Mutex::new(MacPlatformState {
+ dispatcher: Arc::new(MacDispatcher),
+ text_system: Arc::new(MacTextSystem::new()),
+ pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
+ text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
+ metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
+ become_active: None,
+ resign_active: None,
+ reopen: None,
+ quit: None,
+ event: None,
+ will_open_menu: None,
+ open_urls: None,
+ finish_launching: None,
+ // menu_command: None,
+ // validate_menu_command: None,
+ // menu_actions: Default::default(),
+ }))
+ }
+
+ unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
+ let pasteboard = self.0.lock().pasteboard;
+ let data = pasteboard.dataForType(kind);
+ if data == nil {
+ None
+ } else {
+ Some(slice::from_raw_parts(
+ data.bytes() as *mut u8,
+ data.length() as usize,
+ ))
+ }
+ }
+
+ // unsafe fn create_menu_bar(
+ // &self,
+ // menus: Vec<Menu>,
+ // delegate: id,
+ // actions: &mut Vec<Box<dyn Action>>,
+ // keystroke_matcher: &KeymapMatcher,
+ // ) -> id {
+ // let application_menu = NSMenu::new(nil).autorelease();
+ // application_menu.setDelegate_(delegate);
+
+ // for menu_config in menus {
+ // let menu = NSMenu::new(nil).autorelease();
+ // menu.setTitle_(ns_string(menu_config.name));
+ // menu.setDelegate_(delegate);
+
+ // for item_config in menu_config.items {
+ // menu.addItem_(self.create_menu_item(
+ // item_config,
+ // delegate,
+ // actions,
+ // keystroke_matcher,
+ // ));
+ // }
+
+ // let menu_item = NSMenuItem::new(nil).autorelease();
+ // menu_item.setSubmenu_(menu);
+ // application_menu.addItem_(menu_item);
+
+ // if menu_config.name == "Window" {
+ // let app: id = msg_send![APP_CLASS, sharedApplication];
+ // app.setWindowsMenu_(menu);
+ // }
+ // }
+
+ // application_menu
+ // }
+
+ // unsafe fn create_menu_item(
+ // &self,
+ // item: MenuItem,
+ // delegate: id,
+ // actions: &mut Vec<Box<dyn Action>>,
+ // keystroke_matcher: &KeymapMatcher,
+ // ) -> id {
+ // match item {
+ // MenuItem::Separator => NSMenuItem::separatorItem(nil),
+ // MenuItem::Action {
+ // name,
+ // action,
+ // os_action,
+ // } => {
+ // // TODO
+ // let keystrokes = keystroke_matcher
+ // .bindings_for_action(action.id())
+ // .find(|binding| binding.action().eq(action.as_ref()))
+ // .map(|binding| binding.keystrokes());
+ // let selector = match os_action {
+ // Some(crate::OsAction::Cut) => selector("cut:"),
+ // Some(crate::OsAction::Copy) => selector("copy:"),
+ // Some(crate::OsAction::Paste) => selector("paste:"),
+ // Some(crate::OsAction::SelectAll) => selector("selectAll:"),
+ // Some(crate::OsAction::Undo) => selector("undo:"),
+ // Some(crate::OsAction::Redo) => selector("redo:"),
+ // None => selector("handleGPUIMenuItem:"),
+ // };
+
+ // let item;
+ // if let Some(keystrokes) = keystrokes {
+ // if keystrokes.len() == 1 {
+ // let keystroke = &keystrokes[0];
+ // let mut mask = NSEventModifierFlags::empty();
+ // for (modifier, flag) in &[
+ // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
+ // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
+ // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
+ // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
+ // ] {
+ // if *modifier {
+ // mask |= *flag;
+ // }
+ // }
+
+ // item = NSMenuItem::alloc(nil)
+ // .initWithTitle_action_keyEquivalent_(
+ // ns_string(name),
+ // selector,
+ // ns_string(key_to_native(&keystroke.key).as_ref()),
+ // )
+ // .autorelease();
+ // item.setKeyEquivalentModifierMask_(mask);
+ // }
+ // // For multi-keystroke bindings, render the keystroke as part of the title.
+ // else {
+ // use std::fmt::Write;
+
+ // let mut name = format!("{name} [");
+ // for (i, keystroke) in keystrokes.iter().enumerate() {
+ // if i > 0 {
+ // name.push(' ');
+ // }
+ // write!(&mut name, "{}", keystroke).unwrap();
+ // }
+ // name.push(']');
+
+ // item = NSMenuItem::alloc(nil)
+ // .initWithTitle_action_keyEquivalent_(
+ // ns_string(&name),
+ // selector,
+ // ns_string(""),
+ // )
+ // .autorelease();
+ // }
+ // } else {
+ // item = NSMenuItem::alloc(nil)
+ // .initWithTitle_action_keyEquivalent_(
+ // ns_string(name),
+ // selector,
+ // ns_string(""),
+ // )
+ // .autorelease();
+ // }
+
+ // let tag = actions.len() as NSInteger;
+ // let _: () = msg_send![item, setTag: tag];
+ // actions.push(action);
+ // item
+ // }
+ // MenuItem::Submenu(Menu { name, items }) => {
+ // let item = NSMenuItem::new(nil).autorelease();
+ // let submenu = NSMenu::new(nil).autorelease();
+ // submenu.setDelegate_(delegate);
+ // for item in items {
+ // submenu.addItem_(self.create_menu_item(
+ // item,
+ // delegate,
+ // actions,
+ // keystroke_matcher,
+ // ));
+ // }
+ // item.setSubmenu_(submenu);
+ // item.setTitle_(ns_string(name));
+ // item
+ // }
+ // }
+ // }
+}
+
+impl Platform for MacPlatform {
+ fn dispatcher(&self) -> Arc<dyn crate::PlatformDispatcher> {
+ Arc::new(MacDispatcher)
+ }
+
+ fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
+ self.0.lock().text_system.clone()
+ }
+
+ fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
+ self.0.lock().finish_launching = Some(on_finish_launching);
+
+ unsafe {
+ let app: id = msg_send![APP_CLASS, sharedApplication];
+ let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
+ app.setDelegate_(app_delegate);
+
+ let self_ptr = self as *const Self as *const c_void;
+ (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
+ (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
+
+ let pool = NSAutoreleasePool::new(nil);
+ app.run();
+ pool.drain();
+
+ (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+ (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
+ }
+ }
+
+ fn quit(&self) {
+ // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
+ // synchronously before this method terminates. If we call `Platform::quit` while holding a
+ // borrow of the app state (which most of the time we will do), we will end up
+ // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
+ // this, we make quitting the application asynchronous so that we aren't holding borrows to
+ // the app state on the stack when we actually terminate the app.
+
+ use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
+
+ unsafe {
+ dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
+ }
+
+ unsafe extern "C" fn quit(_: *mut c_void) {
+ let app = NSApplication::sharedApplication(nil);
+ let _: () = msg_send![app, terminate: nil];
+ }
+ }
+
+ fn restart(&self) {
+ use std::os::unix::process::CommandExt as _;
+
+ let app_pid = std::process::id().to_string();
+ let app_path = self
+ .app_path()
+ .ok()
+ // When the app is not bundled, `app_path` returns the
+ // directory containing the executable. Disregard this
+ // and get the path to the executable itself.
+ .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path))
+ .unwrap_or_else(|| std::env::current_exe().unwrap());
+
+ // Wait until this process has exited and then re-open this path.
+ let script = r#"
+ while kill -0 $0 2> /dev/null; do
+ sleep 0.1
+ done
+ open "$1"
+ "#;
+
+ let restart_process = Command::new("/bin/bash")
+ .arg("-c")
+ .arg(script)
+ .arg(app_pid)
+ .arg(app_path)
+ .process_group(0)
+ .spawn();
+
+ match restart_process {
+ Ok(_) => self.quit(),
+ Err(e) => log::error!("failed to spawn restart script: {:?}", e),
+ }
+ }
+
+ fn activate(&self, ignoring_other_apps: bool) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
+ }
+ }
+
+ fn hide(&self) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let _: () = msg_send![app, hide: nil];
+ }
+ }
+
+ fn hide_other_apps(&self) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let _: () = msg_send![app, hideOtherApplications: nil];
+ }
+ }
+
+ fn unhide_other_apps(&self) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let _: () = msg_send![app, unhideAllApplications: nil];
+ }
+ }
+
+ fn screens(&self) -> Vec<Rc<dyn PlatformScreen>> {
+ MacScreen::all()
+ .into_iter()
+ .map(|screen| Rc::new(screen) as Rc<_>)
+ .collect()
+ }
+
+ fn screen_by_id(&self, id: ScreenId) -> Option<Rc<dyn PlatformScreen>> {
+ MacScreen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
+ }
+
+ // fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
+ // Box::new(StatusItem::add(self.fonts()))
+ // }
+
+ fn main_window(&self) -> Option<AnyWindowHandle> {
+ MacWindow::main_window()
+ }
+
+ fn open_window(
+ &self,
+ handle: AnyWindowHandle,
+ options: WindowOptions,
+ ) -> Box<dyn PlatformWindow> {
+ Box::new(MacWindow::open(handle, options, self))
+ }
+
+ fn open_url(&self, url: &str) {
+ unsafe {
+ let url = NSURL::alloc(nil)
+ .initWithString_(ns_string(url))
+ .autorelease();
+ let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
+ msg_send![workspace, openURL: url]
+ }
+ }
+
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+ self.0.lock().open_urls = Some(callback);
+ }
+
+ fn prompt_for_paths(
+ &self,
+ options: PathPromptOptions,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+ unsafe {
+ let panel = NSOpenPanel::openPanel(nil);
+ panel.setCanChooseDirectories_(options.directories.to_objc());
+ panel.setCanChooseFiles_(options.files.to_objc());
+ panel.setAllowsMultipleSelection_(options.multiple.to_objc());
+ panel.setResolvesAliases_(false.to_objc());
+ let (done_tx, done_rx) = oneshot::channel();
+ let done_tx = Cell::new(Some(done_tx));
+ let block = ConcreteBlock::new(move |response: NSModalResponse| {
+ let result = if response == NSModalResponse::NSModalResponseOk {
+ let mut result = Vec::new();
+ let urls = panel.URLs();
+ for i in 0..urls.count() {
+ let url = urls.objectAtIndex(i);
+ if url.isFileURL() == YES {
+ if let Ok(path) = ns_url_to_path(url) {
+ result.push(path)
+ }
+ }
+ }
+ Some(result)
+ } else {
+ None
+ };
+
+ if let Some(done_tx) = done_tx.take() {
+ let _ = done_tx.send(result);
+ }
+ });
+ let block = block.copy();
+ let _: () = msg_send![panel, beginWithCompletionHandler: block];
+ done_rx
+ }
+ }
+
+ fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+ unsafe {
+ let panel = NSSavePanel::savePanel(nil);
+ let path = ns_string(directory.to_string_lossy().as_ref());
+ let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
+ panel.setDirectoryURL(url);
+
+ let (done_tx, done_rx) = oneshot::channel();
+ let done_tx = Cell::new(Some(done_tx));
+ let block = ConcreteBlock::new(move |response: NSModalResponse| {
+ let mut result = None;
+ if response == NSModalResponse::NSModalResponseOk {
+ let url = panel.URL();
+ if url.isFileURL() == YES {
+ result = ns_url_to_path(panel.URL()).ok()
+ }
+ }
+
+ if let Some(done_tx) = done_tx.take() {
+ let _ = done_tx.send(result);
+ }
+ });
+ let block = block.copy();
+ let _: () = msg_send![panel, beginWithCompletionHandler: block];
+ done_rx
+ }
+ }
+
+ fn reveal_path(&self, path: &Path) {
+ unsafe {
+ let path = path.to_path_buf();
+ let dispatcher = self.0.lock().dispatcher.clone();
+ let _ = crate::spawn_on_main_local(dispatcher, async move {
+ let full_path = ns_string(path.to_str().unwrap_or(""));
+ let root_full_path = ns_string("");
+ let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
+ let _: BOOL = msg_send![
+ workspace,
+ selectFile: full_path
+ inFileViewerRootedAtPath: root_full_path
+ ];
+ });
+ }
+ }
+
+ fn on_become_active(&self, callback: Box<dyn FnMut()>) {
+ self.0.lock().become_active = Some(callback);
+ }
+
+ fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
+ self.0.lock().resign_active = Some(callback);
+ }
+
+ fn on_quit(&self, callback: Box<dyn FnMut()>) {
+ self.0.lock().quit = Some(callback);
+ }
+
+ fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+ self.0.lock().reopen = Some(callback);
+ }
+
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>) {
+ self.0.lock().event = Some(callback);
+ }
+
+ fn os_name(&self) -> &'static str {
+ "macOS"
+ }
+
+ fn os_version(&self) -> Result<SemanticVersion> {
+ unsafe {
+ let process_info = NSProcessInfo::processInfo(nil);
+ let version = process_info.operatingSystemVersion();
+ Ok(SemanticVersion {
+ major: version.majorVersion as usize,
+ minor: version.minorVersion as usize,
+ patch: version.patchVersion as usize,
+ })
+ }
+ }
+
+ fn app_version(&self) -> Result<SemanticVersion> {
+ unsafe {
+ let bundle: id = NSBundle::mainBundle();
+ if bundle.is_null() {
+ Err(anyhow!("app is not running inside a bundle"))
+ } else {
+ let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")];
+ let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
+ let bytes = version.UTF8String() as *const u8;
+ let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
+ version.parse()
+ }
+ }
+ }
+
+ fn app_path(&self) -> Result<PathBuf> {
+ unsafe {
+ let bundle: id = NSBundle::mainBundle();
+ if bundle.is_null() {
+ Err(anyhow!("app is not running inside a bundle"))
+ } else {
+ Ok(path_from_objc(msg_send![bundle, bundlePath]))
+ }
+ }
+ }
+
+ fn local_timezone(&self) -> UtcOffset {
+ unsafe {
+ let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
+ let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
+ UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
+ }
+ }
+
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
+ unsafe {
+ let bundle: id = NSBundle::mainBundle();
+ if bundle.is_null() {
+ Err(anyhow!("app is not running inside a bundle"))
+ } else {
+ let name = ns_string(name);
+ let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
+ if url.is_null() {
+ Err(anyhow!("resource not found"))
+ } else {
+ ns_url_to_path(url)
+ }
+ }
+ }
+ }
+
+ fn set_cursor_style(&self, style: CursorStyle) {
+ unsafe {
+ let new_cursor: id = match style {
+ CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
+ CursorStyle::ResizeLeftRight => {
+ msg_send![class!(NSCursor), resizeLeftRightCursor]
+ }
+ CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
+ CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+ CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
+ };
+
+ let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
+ if new_cursor != old_cursor {
+ let _: () = msg_send![new_cursor, set];
+ }
+ }
+ }
+
+ fn should_auto_hide_scrollbars(&self) -> bool {
+ #[allow(non_upper_case_globals)]
+ const NSScrollerStyleOverlay: NSInteger = 1;
+
+ unsafe {
+ let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
+ style == NSScrollerStyleOverlay
+ }
+ }
+
+ fn write_to_clipboard(&self, item: ClipboardItem) {
+ let state = self.0.lock();
+ unsafe {
+ state.pasteboard.clearContents();
+
+ let text_bytes = NSData::dataWithBytes_length_(
+ nil,
+ item.text.as_ptr() as *const c_void,
+ item.text.len() as u64,
+ );
+ state
+ .pasteboard
+ .setData_forType(text_bytes, NSPasteboardTypeString);
+
+ if let Some(metadata) = item.metadata.as_ref() {
+ let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
+ let hash_bytes = NSData::dataWithBytes_length_(
+ nil,
+ hash_bytes.as_ptr() as *const c_void,
+ hash_bytes.len() as u64,
+ );
+ state
+ .pasteboard
+ .setData_forType(hash_bytes, state.text_hash_pasteboard_type);
+
+ let metadata_bytes = NSData::dataWithBytes_length_(
+ nil,
+ metadata.as_ptr() as *const c_void,
+ metadata.len() as u64,
+ );
+ state
+ .pasteboard
+ .setData_forType(metadata_bytes, state.metadata_pasteboard_type);
+ }
+ }
+ }
+
+ fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+ let state = self.0.lock();
+ unsafe {
+ if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
+ let text = String::from_utf8_lossy(text_bytes).to_string();
+ let hash_bytes = self
+ .read_from_pasteboard(state.text_hash_pasteboard_type)
+ .and_then(|bytes| bytes.try_into().ok())
+ .map(u64::from_be_bytes);
+ let metadata_bytes = self
+ .read_from_pasteboard(state.metadata_pasteboard_type)
+ .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
+
+ if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
+ if hash == ClipboardItem::text_hash(&text) {
+ Some(ClipboardItem {
+ text,
+ metadata: Some(metadata),
+ })
+ } else {
+ Some(ClipboardItem {
+ text,
+ metadata: None,
+ })
+ }
+ } else {
+ Some(ClipboardItem {
+ text,
+ metadata: None,
+ })
+ }
+ } else {
+ None
+ }
+ }
+ }
+
+ // fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
+ // self.0.lock().menu_command = Some(callback);
+ // }
+
+ // fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+ // self.0.lock().will_open_menu = Some(callback);
+ // }
+
+ // fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+ // self.0.lock().validate_menu_command = Some(callback);
+ // }
+
+ // fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
+ // unsafe {
+ // let app: id = msg_send![APP_CLASS, sharedApplication];
+ // let mut state = self.0.lock();
+ // let actions = &mut state.menu_actions;
+ // app.setMainMenu_(self.create_menu_bar(
+ // menus,
+ // app.delegate(),
+ // actions,
+ // keystroke_matcher,
+ // ));
+ // }
+ // }
+
+ fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
+ let url = CFString::from(url);
+ let username = CFString::from(username);
+ let password = CFData::from_buffer(password);
+
+ unsafe {
+ use security::*;
+
+ // First, check if there are already credentials for the given server. If so, then
+ // update the username and password.
+ let mut verb = "updating";
+ let mut query_attrs = CFMutableDictionary::with_capacity(2);
+ query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+ query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+ let mut attrs = CFMutableDictionary::with_capacity(4);
+ attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+ attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+ attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
+ attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
+
+ let mut status = SecItemUpdate(
+ query_attrs.as_concrete_TypeRef(),
+ attrs.as_concrete_TypeRef(),
+ );
+
+ // If there were no existing credentials for the given server, then create them.
+ if status == errSecItemNotFound {
+ verb = "creating";
+ status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
+ }
+
+ if status != errSecSuccess {
+ return Err(anyhow!("{} password failed: {}", verb, status));
+ }
+ }
+ Ok(())
+ }
+
+ fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
+ let url = CFString::from(url);
+ let cf_true = CFBoolean::true_value().as_CFTypeRef();
+
+ unsafe {
+ use security::*;
+
+ // Find any credentials for the given server URL.
+ let mut attrs = CFMutableDictionary::with_capacity(5);
+ attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+ attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+ attrs.set(kSecReturnAttributes as *const _, cf_true);
+ attrs.set(kSecReturnData as *const _, cf_true);
+
+ let mut result = CFTypeRef::from(ptr::null());
+ let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
+ match status {
+ security::errSecSuccess => {}
+ security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
+ _ => return Err(anyhow!("reading password failed: {}", status)),
+ }
+
+ let result = CFType::wrap_under_create_rule(result)
+ .downcast::<CFDictionary>()
+ .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
+ let username = result
+ .find(kSecAttrAccount as *const _)
+ .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
+ let username = CFType::wrap_under_get_rule(*username)
+ .downcast::<CFString>()
+ .ok_or_else(|| anyhow!("account was not a string"))?;
+ let password = result
+ .find(kSecValueData as *const _)
+ .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
+ let password = CFType::wrap_under_get_rule(*password)
+ .downcast::<CFData>()
+ .ok_or_else(|| anyhow!("password was not a string"))?;
+
+ Ok(Some((username.to_string(), password.bytes().to_vec())))
+ }
+ }
+
+ fn delete_credentials(&self, url: &str) -> Result<()> {
+ let url = CFString::from(url);
+
+ unsafe {
+ use security::*;
+
+ let mut query_attrs = CFMutableDictionary::with_capacity(2);
+ query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
+ query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
+
+ let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
+
+ if status != errSecSuccess {
+ return Err(anyhow!("delete password failed: {}", status));
+ }
+ }
+ Ok(())
+ }
+}
+
+unsafe fn path_from_objc(path: id) -> PathBuf {
+ let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
+ let bytes = path.UTF8String() as *const u8;
+ let path = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
+ PathBuf::from(path)
+}
+
+unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform {
+ let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
+ assert!(!platform_ptr.is_null());
+ &*(platform_ptr as *const MacPlatform)
+}
+
+extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
+ unsafe {
+ if let Some(event) = Event::from_native(native_event, None) {
+ let platform = get_foreground_platform(this);
+ if let Some(callback) = platform.0.lock().event.as_mut() {
+ if callback(event) {
+ return;
+ }
+ }
+ }
+ msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
+ }
+}
+
+extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
+ unsafe {
+ let app: id = msg_send![APP_CLASS, sharedApplication];
+ app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
+
+ let platform = get_foreground_platform(this);
+ let callback = platform.0.lock().finish_launching.take();
+ if let Some(callback) = callback {
+ callback();
+ }
+ }
+}
+
+extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
+ if !has_open_windows {
+ let platform = unsafe { get_foreground_platform(this) };
+ if let Some(callback) = platform.0.lock().reopen.as_mut() {
+ callback();
+ }
+ }
+}
+
+extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
+ let platform = unsafe { get_foreground_platform(this) };
+ if let Some(callback) = platform.0.lock().become_active.as_mut() {
+ callback();
+ }
+}
+
+extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
+ let platform = unsafe { get_foreground_platform(this) };
+ if let Some(callback) = platform.0.lock().resign_active.as_mut() {
+ callback();
+ }
+}
+
+extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
+ let platform = unsafe { get_foreground_platform(this) };
+ if let Some(callback) = platform.0.lock().quit.as_mut() {
+ callback();
+ }
+}
+
+extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
+ let urls = unsafe {
+ (0..urls.count())
+ .into_iter()
+ .filter_map(|i| {
+ let url = urls.objectAtIndex(i);
+ match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() {
+ Ok(string) => Some(string.to_string()),
+ Err(err) => {
+ log::error!("error converting path to string: {}", err);
+ None
+ }
+ }
+ })
+ .collect::<Vec<_>>()
+ };
+ let platform = unsafe { get_foreground_platform(this) };
+ if let Some(callback) = platform.0.lock().open_urls.as_mut() {
+ callback(urls);
+ }
+}
+
+extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) {
+ todo!()
+ // unsafe {
+ // let platform = get_foreground_platform(this);
+ // let mut platform = platform.0.lock();
+ // if let Some(mut callback) = platform.menu_command.take() {
+ // let tag: NSInteger = msg_send![item, tag];
+ // let index = tag as usize;
+ // if let Some(action) = platform.menu_actions.get(index) {
+ // callback(action.as_ref());
+ // }
+ // platform.menu_command = Some(callback);
+ // }
+ // }
+}
+
+extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool {
+ todo!()
+ // unsafe {
+ // let mut result = false;
+ // let platform = get_foreground_platform(this);
+ // let mut platform = platform.0.lock();
+ // if let Some(mut callback) = platform.validate_menu_command.take() {
+ // let tag: NSInteger = msg_send![item, tag];
+ // let index = tag as usize;
+ // if let Some(action) = platform.menu_actions.get(index) {
+ // result = callback(action.as_ref());
+ // }
+ // platform.validate_menu_command = Some(callback);
+ // }
+ // result
+ // }
+}
+
+extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
+ unsafe {
+ let platform = get_foreground_platform(this);
+ let mut platform = platform.0.lock();
+ if let Some(mut callback) = platform.will_open_menu.take() {
+ callback();
+ platform.will_open_menu = Some(callback);
+ }
+ }
+}
+
+unsafe fn ns_string(string: &str) -> id {
+ NSString::alloc(nil).init_str(string).autorelease()
+}
+
+unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
+ let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+ if path.is_null() {
+ Err(anyhow!(
+ "url is not a file path: {}",
+ CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+ ))
+ } else {
+ Ok(PathBuf::from(OsStr::from_bytes(
+ CStr::from_ptr(path).to_bytes(),
+ )))
+ }
+}
+
+mod security {
+ #![allow(non_upper_case_globals)]
+ use super::*;
+
+ #[link(name = "Security", kind = "framework")]
+ extern "C" {
+ pub static kSecClass: CFStringRef;
+ pub static kSecClassInternetPassword: CFStringRef;
+ pub static kSecAttrServer: CFStringRef;
+ pub static kSecAttrAccount: CFStringRef;
+ pub static kSecValueData: CFStringRef;
+ pub static kSecReturnAttributes: CFStringRef;
+ pub static kSecReturnData: CFStringRef;
+
+ pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
+ pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
+ pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
+ pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
+ }
+
+ pub const errSecSuccess: OSStatus = 0;
+ pub const errSecUserCanceled: OSStatus = -128;
+ pub const errSecItemNotFound: OSStatus = -25300;
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::ClipboardItem;
+
+ use super::*;
+
+ #[test]
+ fn test_clipboard() {
+ let platform = build_platform();
+ assert_eq!(platform.read_from_clipboard(), None);
+
+ let item = ClipboardItem::new("1".to_string());
+ platform.write_to_clipboard(item.clone());
+ assert_eq!(platform.read_from_clipboard(), Some(item));
+
+ let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
+ platform.write_to_clipboard(item.clone());
+ assert_eq!(platform.read_from_clipboard(), Some(item));
+
+ let text_from_other_app = "text from other app";
+ unsafe {
+ let bytes = NSData::dataWithBytes_length_(
+ nil,
+ text_from_other_app.as_ptr() as *const c_void,
+ text_from_other_app.len() as u64,
+ );
+ platform
+ .0
+ .lock()
+ .pasteboard
+ .setData_forType(bytes, NSPasteboardTypeString);
+ }
+ assert_eq!(
+ platform.read_from_clipboard(),
+ Some(ClipboardItem::new(text_from_other_app.to_string()))
+ );
+ }
+
+ fn build_platform() -> MacPlatform {
+ let platform = MacPlatform::new();
+ platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
+ platform
+ }
+}
@@ -0,0 +1,156 @@
+use super::ns_string;
+use crate::{point, px, size, Bounds, Pixels, PlatformScreen, PlatformScreenHandle, ScreenId};
+use cocoa::{
+ appkit::NSScreen,
+ base::{id, nil},
+ foundation::{NSArray, NSDictionary, NSPoint, NSRect, NSSize},
+};
+use core_foundation::{
+ number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef},
+ uuid::{CFUUIDGetUUIDBytes, CFUUIDRef},
+};
+use core_graphics::display::CGDirectDisplayID;
+use objc::runtime::Object;
+use std::{any::Any, ffi::c_void};
+use uuid::Uuid;
+
+#[link(name = "ApplicationServices", kind = "framework")]
+extern "C" {
+ pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
+}
+
+#[derive(Debug)]
+pub struct MacScreen {
+ pub(crate) native_screen: id,
+}
+
+unsafe impl Send for MacScreen {}
+
+impl MacScreen {
+ pub(crate) fn from_handle(handle: PlatformScreenHandle) -> Self {
+ Self {
+ native_screen: handle.0 as *mut Object,
+ }
+ }
+
+ /// Get the screen with the given UUID.
+ pub fn find_by_id(id: ScreenId) -> Option<Self> {
+ Self::all().find(|screen| screen.id() == Some(id))
+ }
+
+ /// Get the primary screen - the one with the menu bar, and whose bottom left
+ /// corner is at the origin of the AppKit coordinate system.
+ fn primary() -> Self {
+ Self::all().next().unwrap()
+ }
+
+ pub fn all() -> impl Iterator<Item = Self> {
+ unsafe {
+ let native_screens = NSScreen::screens(nil);
+ (0..NSArray::count(native_screens)).map(move |ix| MacScreen {
+ native_screen: native_screens.objectAtIndex(ix),
+ })
+ }
+ }
+
+ /// Convert the given rectangle in screen coordinates from GPUI's
+ /// coordinate system to the AppKit coordinate system.
+ ///
+ /// In GPUI's coordinates, the origin is at the top left of the primary screen, with
+ /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
+ /// bottom left of the primary screen, with the Y axis pointing upward.
+ pub(crate) fn screen_bounds_to_native(bounds: Bounds<Pixels>) -> NSRect {
+ let primary_screen_height =
+ px(unsafe { Self::primary().native_screen.frame().size.height } as f32);
+
+ NSRect::new(
+ NSPoint::new(
+ bounds.origin.x.into(),
+ (primary_screen_height - bounds.origin.y - bounds.size.height).into(),
+ ),
+ NSSize::new(bounds.size.width.into(), bounds.size.height.into()),
+ )
+ }
+
+ /// Convert the given rectangle in screen coordinates from the AppKit
+ /// coordinate system to GPUI's coordinate system.
+ ///
+ /// In GPUI's coordinates, the origin is at the top left of the primary screen, with
+ /// the Y axis pointing downward. In the AppKit coordindate system, the origin is at the
+ /// bottom left of the primary screen, with the Y axis pointing upward.
+ pub(crate) fn screen_bounds_from_native(rect: NSRect) -> Bounds<Pixels> {
+ let primary_screen_height = unsafe { Self::primary().native_screen.frame().size.height };
+ Bounds {
+ origin: point(
+ px(rect.origin.x as f32),
+ px((primary_screen_height - rect.origin.y - rect.size.height) as f32),
+ ),
+ size: size(px(rect.size.width as f32), px(rect.size.height as f32)),
+ }
+ }
+}
+
+impl PlatformScreen for MacScreen {
+ fn id(&self) -> Option<ScreenId> {
+ unsafe {
+ // This approach is similar to that which winit takes
+ // https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99
+ let device_description = self.native_screen.deviceDescription();
+
+ let key = ns_string("NSScreenNumber");
+ let device_id_obj = device_description.objectForKey_(key);
+ if device_id_obj.is_null() {
+ // Under some circumstances, especially display re-arrangements or display locking, we seem to get a null pointer
+ // to the device id. See: https://linear.app/zed-industries/issue/Z-257/lock-screen-crash-with-multiple-monitors
+ return None;
+ }
+
+ let mut device_id: u32 = 0;
+ CFNumberGetValue(
+ device_id_obj as CFNumberRef,
+ kCFNumberIntType,
+ (&mut device_id) as *mut _ as *mut c_void,
+ );
+ let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID);
+ if cfuuid.is_null() {
+ return None;
+ }
+
+ let bytes = CFUUIDGetUUIDBytes(cfuuid);
+ Some(ScreenId(Uuid::from_bytes([
+ bytes.byte0,
+ bytes.byte1,
+ bytes.byte2,
+ bytes.byte3,
+ bytes.byte4,
+ bytes.byte5,
+ bytes.byte6,
+ bytes.byte7,
+ bytes.byte8,
+ bytes.byte9,
+ bytes.byte10,
+ bytes.byte11,
+ bytes.byte12,
+ bytes.byte13,
+ bytes.byte14,
+ bytes.byte15,
+ ])))
+ }
+ }
+
+ fn handle(&self) -> PlatformScreenHandle {
+ PlatformScreenHandle(self.native_screen as *mut c_void)
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn bounds(&self) -> Bounds<Pixels> {
+ unsafe { Self::screen_bounds_from_native(self.native_screen.frame()) }
+ }
+
+ fn content_bounds(&self) -> Bounds<Pixels> {
+ unsafe { Self::screen_bounds_from_native(self.native_screen.visibleFrame()) }
+ }
+}
@@ -0,0 +1,180 @@
+#include <metal_stdlib>
+#include <simd/simd.h>
+
+using namespace metal;
+
+float4 hsla_to_rgba(Hsla hsla);
+float4 to_device_position(float2 pixel_position, float2 viewport_size);
+
+struct QuadVertexOutput {
+ float4 position [[position]];
+ float4 background_color;
+ float4 border_color;
+ uint quad_id;
+};
+
+vertex QuadVertexOutput quad_vertex(
+ uint unit_vertex_id [[vertex_id]],
+ uint quad_id [[instance_id]],
+ constant float2 *unit_vertices [[buffer(QuadInputIndex_Vertices)]],
+ constant Quad *quads [[buffer(QuadInputIndex_Quads)]],
+ constant QuadUniforms *uniforms [[buffer(QuadInputIndex_Uniforms)]]
+) {
+ float2 unit_vertex = unit_vertices[unit_vertex_id];
+ Quad quad = quads[quad_id];
+ float2 position_2d = unit_vertex * float2(quad.bounds.size.width, quad.bounds.size.height) + float2(quad.bounds.origin.x, quad.bounds.origin.y);
+ position_2d.x = max(quad.clip_bounds.origin.x, position_2d.x);
+ position_2d.x = min(quad.clip_bounds.origin.x + quad.clip_bounds.size.width, position_2d.x);
+ position_2d.y = max(quad.clip_bounds.origin.y, position_2d.y);
+ position_2d.y = min(quad.clip_bounds.origin.y + quad.clip_bounds.size.height, position_2d.y);
+
+ float2 viewport_size = float2((float)uniforms->viewport_size.width, (float)uniforms->viewport_size.height);
+ float4 device_position = to_device_position(position_2d, viewport_size);
+ float4 background_color = hsla_to_rgba(quad.background);
+ float4 border_color = hsla_to_rgba(quad.border_color);
+ return QuadVertexOutput {
+ device_position,
+ background_color,
+ border_color,
+ quad_id
+ };
+}
+
+float quad_sdf(float2 point, Bounds_Pixels bounds, Corners_Pixels corner_radii) {
+ float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
+ float2 center = float2(bounds.origin.x, bounds.origin.y) + half_size;
+ float2 center_to_point = point - center;
+ float corner_radius;
+ if (center_to_point.x < 0.) {
+ if (center_to_point.y < 0.) {
+ corner_radius = corner_radii.top_left;
+ } else {
+ corner_radius = corner_radii.bottom_left;
+ }
+ } else {
+ if (center_to_point.y < 0.) {
+ corner_radius = corner_radii.top_right;
+ } else {
+ corner_radius = corner_radii.bottom_right;
+ }
+ }
+
+ float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
+ float distance = length(max(0., rounded_edge_to_point))
+ + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y))
+ - corner_radius;
+
+ return distance;
+}
+
+fragment float4 quad_fragment(
+ QuadVertexOutput input [[stage_in]],
+ constant Quad *quads [[buffer(QuadInputIndex_Quads)]]
+) {
+ Quad quad = quads[input.quad_id];
+ float2 half_size = float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
+ float2 center = float2(quad.bounds.origin.x, quad.bounds.origin.y) + half_size;
+ float2 center_to_point = input.position.xy - center;
+ float corner_radius;
+ if (center_to_point.x < 0.) {
+ if (center_to_point.y < 0.) {
+ corner_radius = quad.corner_radii.top_left;
+ } else {
+ corner_radius = quad.corner_radii.bottom_left;
+ }
+ } else {
+ if (center_to_point.y < 0.) {
+ corner_radius = quad.corner_radii.top_right;
+ } else {
+ corner_radius = quad.corner_radii.bottom_right;
+ }
+ }
+
+ float2 rounded_edge_to_point = fabs(center_to_point) - half_size + corner_radius;
+ float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - corner_radius;
+
+ float vertical_border = center_to_point.x <= 0. ? quad.border_widths.left : quad.border_widths.right;
+ float horizontal_border = center_to_point.y <= 0. ? quad.border_widths.top : quad.border_widths.bottom;
+ float2 inset_size = half_size - corner_radius - float2(vertical_border, horizontal_border);
+ float2 point_to_inset_corner = fabs(center_to_point) - inset_size;
+ float border_width;
+ if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) {
+ border_width = 0.;
+ } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
+ border_width = horizontal_border;
+ } else {
+ border_width = vertical_border;
+ }
+
+ float4 color;
+ if (border_width == 0.) {
+ color = input.background_color;
+ } else {
+ float inset_distance = distance + border_width;
+
+ // Decrease border's opacity as we move inside the background.
+ input.border_color.a *= 1. - saturate(0.5 - inset_distance);
+
+ // Alpha-blend the border and the background.
+ float output_alpha = quad.border_color.a + quad.background.a * (1. - quad.border_color.a);
+ float3 premultiplied_border_rgb = input.border_color.rgb * quad.border_color.a;
+ float3 premultiplied_background_rgb = input.background_color.rgb * input.background_color.a;
+ float3 premultiplied_output_rgb = premultiplied_border_rgb + premultiplied_background_rgb * (1. - input.border_color.a);
+ color = float4(premultiplied_output_rgb, output_alpha);
+ }
+
+ float clip_distance = quad_sdf(input.position.xy, quad.clip_bounds, quad.clip_corner_radii);
+ return color * float4(1., 1., 1., saturate(0.5 - distance) * saturate(0.5 - clip_distance));
+}
+
+float4 hsla_to_rgba(Hsla hsla) {
+ float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
+ float s = hsla.s;
+ float l = hsla.l;
+ float a = hsla.a;
+
+ float c = (1.0 - fabs(2.0*l - 1.0)) * s;
+ float x = c * (1.0 - fabs(fmod(h, 2.0) - 1.0));
+ float m = l - c/2.0;
+
+ float r = 0.0;
+ float g = 0.0;
+ float b = 0.0;
+
+ if (h >= 0.0 && h < 1.0) {
+ r = c;
+ g = x;
+ b = 0.0;
+ } else if (h >= 1.0 && h < 2.0) {
+ r = x;
+ g = c;
+ b = 0.0;
+ } else if (h >= 2.0 && h < 3.0) {
+ r = 0.0;
+ g = c;
+ b = x;
+ } else if (h >= 3.0 && h < 4.0) {
+ r = 0.0;
+ g = x;
+ b = c;
+ } else if (h >= 4.0 && h < 5.0) {
+ r = x;
+ g = 0.0;
+ b = c;
+ } else {
+ r = c;
+ g = 0.0;
+ b = x;
+ }
+
+ float4 rgba;
+ rgba.x = (r + m);
+ rgba.y = (g + m);
+ rgba.z = (b + m);
+ rgba.w = a;
+ return rgba;
+}
+
+float4 to_device_position(float2 pixel_position, float2 viewport_size) {
+ return float4(pixel_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.);
+}
@@ -0,0 +1,754 @@
+use crate::{
+ point, px, size, Bounds, Font, FontFeatures, FontId, FontMetrics, FontStyle, FontWeight, Glyph,
+ GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RasterizationOptions, Result, Run,
+ SharedString, Size,
+};
+use cocoa::appkit::{CGFloat, CGPoint};
+use collections::HashMap;
+use core_foundation::{
+ array::CFIndex,
+ attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
+ base::{CFRange, TCFType},
+ string::CFString,
+};
+use core_graphics::{
+ base::{kCGImageAlphaPremultipliedLast, CGGlyph},
+ color_space::CGColorSpace,
+ context::CGContext,
+};
+use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
+use font_kit::{
+ font::Font as FontKitFont,
+ handle::Handle,
+ hinting::HintingOptions,
+ metrics::Metrics,
+ properties::{Style as FontkitStyle, Weight as FontkitWeight},
+ source::SystemSource,
+ sources::mem::MemSource,
+};
+use parking_lot::{RwLock, RwLockUpgradableReadGuard};
+use pathfinder_geometry::{
+ rect::{RectF, RectI},
+ transform2d::Transform2F,
+ vector::{Vector2F, Vector2I},
+};
+use smallvec::SmallVec;
+use std::{char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
+
+use super::open_type;
+
+#[allow(non_upper_case_globals)]
+const kCGImageAlphaOnly: u32 = 7;
+
+pub struct MacTextSystem(RwLock<MacTextSystemState>);
+
+struct MacTextSystemState {
+ memory_source: MemSource,
+ system_source: SystemSource,
+ fonts: Vec<FontKitFont>,
+ font_selections: HashMap<Font, FontId>,
+ font_ids_by_postscript_name: HashMap<String, FontId>,
+ font_ids_by_family_name: HashMap<SharedString, SmallVec<[FontId; 4]>>,
+ postscript_names_by_font_id: HashMap<FontId, String>,
+}
+
+impl MacTextSystem {
+ pub fn new() -> Self {
+ Self(RwLock::new(MacTextSystemState {
+ memory_source: MemSource::empty(),
+ system_source: SystemSource::new(),
+ fonts: Vec::new(),
+ font_selections: HashMap::default(),
+ font_ids_by_postscript_name: HashMap::default(),
+ font_ids_by_family_name: HashMap::default(),
+ postscript_names_by_font_id: HashMap::default(),
+ }))
+ }
+}
+
+impl Default for MacTextSystem {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl PlatformTextSystem for MacTextSystem {
+ fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
+ self.0.write().add_fonts(fonts)
+ }
+
+ fn all_font_families(&self) -> Vec<String> {
+ self.0
+ .read()
+ .system_source
+ .all_families()
+ .expect("core text should never return an error")
+ }
+
+ fn font_id(&self, font: &Font) -> Result<FontId> {
+ let lock = self.0.upgradable_read();
+ if let Some(font_id) = lock.font_selections.get(font) {
+ Ok(*font_id)
+ } else {
+ let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
+ let candidates = if let Some(font_ids) = lock.font_ids_by_family_name.get(&font.family)
+ {
+ font_ids.as_slice()
+ } else {
+ let font_ids = lock.load_family(&font.family, font.features)?;
+ lock.font_ids_by_family_name
+ .insert(font.family.clone(), font_ids);
+ lock.font_ids_by_family_name[&font.family].as_ref()
+ };
+
+ let candidate_properties = candidates
+ .iter()
+ .map(|font_id| lock.fonts[font_id.0].properties())
+ .collect::<SmallVec<[_; 4]>>();
+
+ let ix = font_kit::matching::find_best_match(
+ &candidate_properties,
+ &font_kit::properties::Properties {
+ style: font.style.into(),
+ weight: font.weight.into(),
+ stretch: Default::default(),
+ },
+ )?;
+
+ Ok(candidates[ix])
+ }
+ }
+
+ fn font_metrics(&self, font_id: FontId) -> FontMetrics {
+ self.0.read().fonts[font_id.0].metrics().into()
+ }
+
+ fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
+ Ok(self.0.read().fonts[font_id.0]
+ .typographic_bounds(glyph_id.into())?
+ .into())
+ }
+
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
+ self.0.read().advance(font_id, glyph_id)
+ }
+
+ fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+ self.0.read().glyph_for_char(font_id, ch)
+ }
+
+ fn rasterize_glyph(
+ &self,
+ font_id: FontId,
+ font_size: f32,
+ glyph_id: GlyphId,
+ subpixel_shift: Point<Pixels>,
+ scale_factor: f32,
+ options: RasterizationOptions,
+ ) -> Option<(Bounds<u32>, Vec<u8>)> {
+ self.0.read().rasterize_glyph(
+ font_id,
+ font_size,
+ glyph_id,
+ subpixel_shift,
+ scale_factor,
+ options,
+ )
+ }
+
+ fn layout_line(
+ &self,
+ text: &str,
+ font_size: Pixels,
+ font_runs: &[(usize, FontId)],
+ ) -> LineLayout {
+ self.0.write().layout_line(text, font_size, font_runs)
+ }
+
+ fn wrap_line(
+ &self,
+ text: &str,
+ font_id: FontId,
+ font_size: Pixels,
+ width: Pixels,
+ ) -> Vec<usize> {
+ self.0.read().wrap_line(text, font_id, font_size, width)
+ }
+}
+
+impl MacTextSystemState {
+ fn add_fonts(&mut self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
+ self.memory_source.add_fonts(
+ fonts
+ .iter()
+ .map(|bytes| Handle::from_memory(bytes.clone(), 0)),
+ )?;
+ Ok(())
+ }
+
+ fn load_family(
+ &mut self,
+ name: &SharedString,
+ features: FontFeatures,
+ ) -> Result<SmallVec<[FontId; 4]>> {
+ let mut font_ids = SmallVec::new();
+ let family = self
+ .memory_source
+ .select_family_by_name(name.as_ref())
+ .or_else(|_| self.system_source.select_family_by_name(name.as_ref()))?;
+ for font in family.fonts() {
+ let mut font = font.load()?;
+ open_type::apply_features(&mut font, features);
+ let font_id = FontId(self.fonts.len());
+ font_ids.push(font_id);
+ let postscript_name = font.postscript_name().unwrap();
+ self.font_ids_by_postscript_name
+ .insert(postscript_name.clone(), font_id);
+ self.postscript_names_by_font_id
+ .insert(font_id, postscript_name);
+ self.fonts.push(font);
+ }
+ Ok(font_ids)
+ }
+
+ fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
+ Ok(self.fonts[font_id.0].advance(glyph_id.into())?.into())
+ }
+
+ fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
+ self.fonts[font_id.0].glyph_for_char(ch).map(Into::into)
+ }
+
+ fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
+ let postscript_name = requested_font.postscript_name();
+ if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
+ *font_id
+ } else {
+ let font_id = FontId(self.fonts.len());
+ self.font_ids_by_postscript_name
+ .insert(postscript_name.clone(), font_id);
+ self.postscript_names_by_font_id
+ .insert(font_id, postscript_name);
+ self.fonts
+ .push(font_kit::font::Font::from_core_graphics_font(
+ requested_font.copy_to_CGFont(),
+ ));
+ font_id
+ }
+ }
+
+ fn is_emoji(&self, font_id: FontId) -> bool {
+ self.postscript_names_by_font_id
+ .get(&font_id)
+ .map_or(false, |postscript_name| {
+ postscript_name == "AppleColorEmoji"
+ })
+ }
+
+ fn rasterize_glyph(
+ &self,
+ font_id: FontId,
+ font_size: f32,
+ glyph_id: GlyphId,
+ subpixel_shift: Point<Pixels>,
+ scale_factor: f32,
+ options: RasterizationOptions,
+ ) -> Option<(Bounds<u32>, Vec<u8>)> {
+ let font = &self.fonts[font_id.0];
+ let scale = Transform2F::from_scale(scale_factor);
+ let glyph_bounds = font
+ .raster_bounds(
+ glyph_id.into(),
+ font_size,
+ scale,
+ HintingOptions::None,
+ font_kit::canvas::RasterizationOptions::GrayscaleAa,
+ )
+ .ok()?;
+
+ if glyph_bounds.width() == 0 || glyph_bounds.height() == 0 {
+ None
+ } else {
+ // Make room for subpixel variants.
+ let subpixel_padding = subpixel_shift.map(|v| f32::from(v).ceil() as u32);
+ let cx_bounds = RectI::new(
+ glyph_bounds.origin(),
+ glyph_bounds.size() + Vector2I::from(subpixel_padding),
+ );
+
+ let mut bytes;
+ let cx;
+ match options {
+ RasterizationOptions::Alpha => {
+ bytes = vec![0; cx_bounds.width() as usize * cx_bounds.height() as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ cx_bounds.width() as usize,
+ cx_bounds.height() as usize,
+ 8,
+ cx_bounds.width() as usize,
+ &CGColorSpace::create_device_gray(),
+ kCGImageAlphaOnly,
+ );
+ }
+ RasterizationOptions::Bgra => {
+ bytes = vec![0; cx_bounds.width() as usize * 4 * cx_bounds.height() as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ cx_bounds.width() as usize,
+ cx_bounds.height() as usize,
+ 8,
+ cx_bounds.width() as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaPremultipliedLast,
+ );
+ }
+ }
+
+ // Move the origin to bottom left and account for scaling, this
+ // makes drawing text consistent with the font-kit's raster_bounds.
+ cx.translate(
+ -glyph_bounds.origin_x() as CGFloat,
+ (glyph_bounds.origin_y() + glyph_bounds.height()) as CGFloat,
+ );
+ cx.scale(scale_factor as CGFloat, scale_factor as CGFloat);
+
+ cx.set_allows_font_subpixel_positioning(true);
+ cx.set_should_subpixel_position_fonts(true);
+ cx.set_allows_font_subpixel_quantization(false);
+ cx.set_should_subpixel_quantize_fonts(false);
+ font.native_font()
+ .clone_with_font_size(font_size as CGFloat)
+ .draw_glyphs(
+ &[u32::from(glyph_id) as CGGlyph],
+ &[CGPoint::new(
+ (f32::from(subpixel_shift.x) / scale_factor) as CGFloat,
+ (f32::from(subpixel_shift.y) / scale_factor) as CGFloat,
+ )],
+ cx,
+ );
+
+ if let RasterizationOptions::Bgra = options {
+ // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
+ for pixel in bytes.chunks_exact_mut(4) {
+ pixel.swap(0, 2);
+ let a = pixel[3] as f32 / 255.;
+ pixel[0] = (pixel[0] as f32 / a) as u8;
+ pixel[1] = (pixel[1] as f32 / a) as u8;
+ pixel[2] = (pixel[2] as f32 / a) as u8;
+ }
+ }
+
+ Some((cx_bounds.into(), bytes))
+ }
+ }
+
+ fn layout_line(
+ &mut self,
+ text: &str,
+ font_size: Pixels,
+ font_runs: &[(usize, FontId)],
+ ) -> LineLayout {
+ // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
+ let mut string = CFMutableAttributedString::new();
+ {
+ string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+ let utf16_line_len = string.char_len() as usize;
+
+ let mut ix_converter = StringIndexConverter::new(text);
+ for (run_len, font_id) in font_runs {
+ let utf8_end = ix_converter.utf8_ix + run_len;
+ let utf16_start = ix_converter.utf16_ix;
+
+ if utf16_start >= utf16_line_len {
+ break;
+ }
+
+ ix_converter.advance_to_utf8_ix(utf8_end);
+ let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
+
+ let cf_range =
+ CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
+
+ let font: &FontKitFont = &self.fonts[font_id.0];
+ unsafe {
+ string.set_attribute(
+ cf_range,
+ kCTFontAttributeName,
+ &font.native_font().clone_with_font_size(font_size.into()),
+ );
+ }
+
+ if utf16_end == utf16_line_len {
+ break;
+ }
+ }
+ }
+
+ // Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
+ let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
+
+ let mut runs = Vec::new();
+ for run in line.glyph_runs().into_iter() {
+ let attributes = run.attributes().unwrap();
+ let font = unsafe {
+ attributes
+ .get(kCTFontAttributeName)
+ .downcast::<CTFont>()
+ .unwrap()
+ };
+ let font_id = self.id_for_native_font(font);
+
+ let mut ix_converter = StringIndexConverter::new(text);
+ let mut glyphs = Vec::new();
+ for ((glyph_id, position), glyph_utf16_ix) in run
+ .glyphs()
+ .iter()
+ .zip(run.positions().iter())
+ .zip(run.string_indices().iter())
+ {
+ let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
+ ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
+ glyphs.push(Glyph {
+ id: (*glyph_id).into(),
+ position: point(position.x as f32, position.y as f32).map(px),
+ index: ix_converter.utf8_ix,
+ is_emoji: self.is_emoji(font_id),
+ });
+ }
+
+ runs.push(Run { font_id, glyphs })
+ }
+
+ let typographic_bounds = line.get_typographic_bounds();
+ LineLayout {
+ width: typographic_bounds.width.into(),
+ ascent: typographic_bounds.ascent.into(),
+ descent: typographic_bounds.descent.into(),
+ runs,
+ font_size,
+ len: text.len(),
+ }
+ }
+
+ fn wrap_line(
+ &self,
+ text: &str,
+ font_id: FontId,
+ font_size: Pixels,
+ width: Pixels,
+ ) -> Vec<usize> {
+ let mut string = CFMutableAttributedString::new();
+ string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+ let cf_range = CFRange::init(0, text.encode_utf16().count() as isize);
+ let font = &self.fonts[font_id.0];
+ unsafe {
+ string.set_attribute(
+ cf_range,
+ kCTFontAttributeName,
+ &font.native_font().clone_with_font_size(font_size.into()),
+ );
+
+ let typesetter = CTTypesetterCreateWithAttributedString(string.as_concrete_TypeRef());
+ let mut ix_converter = StringIndexConverter::new(text);
+ let mut break_indices = Vec::new();
+ while ix_converter.utf8_ix < text.len() {
+ let utf16_len = CTTypesetterSuggestLineBreak(
+ typesetter,
+ ix_converter.utf16_ix as isize,
+ width.into(),
+ ) as usize;
+ ix_converter.advance_to_utf16_ix(ix_converter.utf16_ix + utf16_len);
+ if ix_converter.utf8_ix >= text.len() {
+ break;
+ }
+ break_indices.push(ix_converter.utf8_ix as usize);
+ }
+ break_indices
+ }
+ }
+}
+
+#[derive(Clone)]
+struct StringIndexConverter<'a> {
+ text: &'a str,
+ utf8_ix: usize,
+ utf16_ix: usize,
+}
+
+impl<'a> StringIndexConverter<'a> {
+ fn new(text: &'a str) -> Self {
+ Self {
+ text,
+ utf8_ix: 0,
+ utf16_ix: 0,
+ }
+ }
+
+ fn advance_to_utf8_ix(&mut self, utf8_target: usize) {
+ for (ix, c) in self.text[self.utf8_ix..].char_indices() {
+ if self.utf8_ix + ix >= utf8_target {
+ self.utf8_ix += ix;
+ return;
+ }
+ self.utf16_ix += c.len_utf16();
+ }
+ self.utf8_ix = self.text.len();
+ }
+
+ fn advance_to_utf16_ix(&mut self, utf16_target: usize) {
+ for (ix, c) in self.text[self.utf8_ix..].char_indices() {
+ if self.utf16_ix >= utf16_target {
+ self.utf8_ix += ix;
+ return;
+ }
+ self.utf16_ix += c.len_utf16();
+ }
+ self.utf8_ix = self.text.len();
+ }
+}
+
+#[repr(C)]
+pub struct __CFTypesetter(c_void);
+
+pub type CTTypesetterRef = *const __CFTypesetter;
+
+#[link(name = "CoreText", kind = "framework")]
+extern "C" {
+ fn CTTypesetterCreateWithAttributedString(string: CFAttributedStringRef) -> CTTypesetterRef;
+
+ fn CTTypesetterSuggestLineBreak(
+ typesetter: CTTypesetterRef,
+ start_index: CFIndex,
+ width: f64,
+ ) -> CFIndex;
+}
+
+impl From<Metrics> for FontMetrics {
+ fn from(metrics: Metrics) -> Self {
+ FontMetrics {
+ units_per_em: metrics.units_per_em,
+ ascent: metrics.ascent,
+ descent: metrics.descent,
+ line_gap: metrics.line_gap,
+ underline_position: metrics.underline_position,
+ underline_thickness: metrics.underline_thickness,
+ cap_height: metrics.cap_height,
+ x_height: metrics.x_height,
+ bounding_box: metrics.bounding_box.into(),
+ }
+ }
+}
+
+impl From<RectF> for Bounds<f32> {
+ fn from(rect: RectF) -> Self {
+ Bounds {
+ origin: point(rect.origin_x(), rect.origin_y()),
+ size: size(rect.width(), rect.height()),
+ }
+ }
+}
+
+impl From<RectI> for Bounds<u32> {
+ fn from(rect: RectI) -> Self {
+ Bounds {
+ origin: point(rect.origin_x() as u32, rect.origin_y() as u32),
+ size: size(rect.width() as u32, rect.height() as u32),
+ }
+ }
+}
+
+impl From<Point<u32>> for Vector2I {
+ fn from(size: Point<u32>) -> Self {
+ Vector2I::new(size.x as i32, size.y as i32)
+ }
+}
+
+impl From<Vector2F> for Size<f32> {
+ fn from(vec: Vector2F) -> Self {
+ size(vec.x(), vec.y())
+ }
+}
+
+impl From<FontWeight> for FontkitWeight {
+ fn from(value: FontWeight) -> Self {
+ FontkitWeight(value.0)
+ }
+}
+
+impl From<FontStyle> for FontkitStyle {
+ fn from(style: FontStyle) -> Self {
+ match style {
+ FontStyle::Normal => FontkitStyle::Normal,
+ FontStyle::Italic => FontkitStyle::Italic,
+ FontStyle::Oblique => FontkitStyle::Oblique,
+ }
+ }
+}
+
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use crate::AppContext;
+// use font_kit::properties::{Style, Weight};
+// use platform::FontSystem as _;
+
+// #[crate::test(self, retries = 5)]
+// fn test_layout_str(_: &mut AppContext) {
+// // This is failing intermittently on CI and we don't have time to figure it out
+// let fonts = FontSystem::new();
+// let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
+// let menlo_regular = RunStyle {
+// font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
+// color: Default::default(),
+// underline: Default::default(),
+// };
+// let menlo_italic = RunStyle {
+// font_id: fonts
+// .select_font(&menlo, Properties::new().style(Style::Italic))
+// .unwrap(),
+// color: Default::default(),
+// underline: Default::default(),
+// };
+// let menlo_bold = RunStyle {
+// font_id: fonts
+// .select_font(&menlo, Properties::new().weight(Weight::BOLD))
+// .unwrap(),
+// color: Default::default(),
+// underline: Default::default(),
+// };
+// assert_ne!(menlo_regular, menlo_italic);
+// assert_ne!(menlo_regular, menlo_bold);
+// assert_ne!(menlo_italic, menlo_bold);
+
+// let line = fonts.layout_line(
+// "hello world",
+// 16.0,
+// &[(2, menlo_bold), (4, menlo_italic), (5, menlo_regular)],
+// );
+// assert_eq!(line.runs.len(), 3);
+// assert_eq!(line.runs[0].font_id, menlo_bold.font_id);
+// assert_eq!(line.runs[0].glyphs.len(), 2);
+// assert_eq!(line.runs[1].font_id, menlo_italic.font_id);
+// assert_eq!(line.runs[1].glyphs.len(), 4);
+// assert_eq!(line.runs[2].font_id, menlo_regular.font_id);
+// assert_eq!(line.runs[2].glyphs.len(), 5);
+// }
+
+// #[test]
+// fn test_glyph_offsets() -> crate::Result<()> {
+// let fonts = FontSystem::new();
+// let zapfino = fonts.load_family("Zapfino", &Default::default())?;
+// let zapfino_regular = RunStyle {
+// font_id: fonts.select_font(&zapfino, &Properties::new())?,
+// color: Default::default(),
+// underline: Default::default(),
+// };
+// let menlo = fonts.load_family("Menlo", &Default::default())?;
+// let menlo_regular = RunStyle {
+// font_id: fonts.select_font(&menlo, &Properties::new())?,
+// color: Default::default(),
+// underline: Default::default(),
+// };
+
+// let text = "This is, m𐍈re 𐍈r less, Zapfino!𐍈";
+// let line = fonts.layout_line(
+// text,
+// 16.0,
+// &[
+// (9, zapfino_regular),
+// (13, menlo_regular),
+// (text.len() - 22, zapfino_regular),
+// ],
+// );
+// assert_eq!(
+// line.runs
+// .iter()
+// .flat_map(|r| r.glyphs.iter())
+// .map(|g| g.index)
+// .collect::<Vec<_>>(),
+// vec![0, 2, 4, 5, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24, 26, 27, 28, 29, 36, 37],
+// );
+// Ok(())
+// }
+
+// #[test]
+// #[ignore]
+// fn test_rasterize_glyph() {
+// use std::{fs::File, io::BufWriter, path::Path};
+
+// let fonts = FontSystem::new();
+// let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
+// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+// let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
+
+// const VARIANTS: usize = 1;
+// for i in 0..VARIANTS {
+// let variant = i as f32 / VARIANTS as f32;
+// let (bounds, bytes) = fonts
+// .rasterize_glyph(
+// font_id,
+// 16.0,
+// glyph_id,
+// vec2f(variant, variant),
+// 2.,
+// RasterizationOptions::Alpha,
+// )
+// .unwrap();
+
+// let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
+// let path = Path::new(&name);
+// let file = File::create(path).unwrap();
+// let w = &mut BufWriter::new(file);
+
+// let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
+// encoder.set_color(png::ColorType::Grayscale);
+// encoder.set_depth(png::BitDepth::Eight);
+// let mut writer = encoder.write_header().unwrap();
+// writer.write_image_data(&bytes).unwrap();
+// }
+// }
+
+// #[test]
+// fn test_wrap_line() {
+// let fonts = FontSystem::new();
+// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
+// let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+
+// let line = "one two three four five\n";
+// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
+// assert_eq!(wrap_boundaries, &["one two ".len(), "one two three ".len()]);
+
+// let line = "aaa ααα ✋✋✋ 🎉🎉🎉\n";
+// let wrap_boundaries = fonts.wrap_line(line, font_id, 16., 64.0);
+// assert_eq!(
+// wrap_boundaries,
+// &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
+// );
+// }
+
+// #[test]
+// fn test_layout_line_bom_char() {
+// let fonts = FontSystem::new();
+// let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
+// let style = RunStyle {
+// font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
+// color: Default::default(),
+// underline: Default::default(),
+// };
+
+// let line = "\u{feff}";
+// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
+// assert_eq!(layout.len, line.len());
+// assert!(layout.runs.is_empty());
+
+// let line = "a\u{feff}b";
+// let layout = fonts.layout_line(line, 16., &[(line.len(), style)]);
+// assert_eq!(layout.len, line.len());
+// assert_eq!(layout.runs.len(), 1);
+// assert_eq!(layout.runs[0].glyphs.len(), 2);
+// assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
+// // There's no glyph for \u{feff}
+// assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
+// }
+// }
@@ -0,0 +1,1595 @@
+use super::{ns_string, MetalRenderer, NSRange};
+use crate::{
+ point, px, size, AnyWindowHandle, Bounds, Event, InputHandler, KeyDownEvent, Keystroke,
+ MacScreen, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMovedEvent,
+ MouseUpEvent, NSRectExt, Pixels, Platform, PlatformDispatcher, PlatformScreen, PlatformWindow,
+ Point, Scene, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
+ WindowPromptLevel,
+};
+use block::ConcreteBlock;
+use cocoa::{
+ appkit::{
+ CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
+ NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
+ NSWindowStyleMask, NSWindowTitleVisibility,
+ },
+ base::{id, nil},
+ foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger},
+};
+use core_graphics::display::CGRect;
+use ctor::ctor;
+use foreign_types::ForeignTypeRef;
+use futures::channel::oneshot;
+use objc::{
+ class,
+ declare::ClassDecl,
+ msg_send,
+ runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
+ sel, sel_impl,
+};
+use parking_lot::Mutex;
+use std::{
+ any::Any,
+ cell::{Cell, RefCell},
+ ffi::{c_void, CStr},
+ mem,
+ ops::Range,
+ os::raw::c_char,
+ ptr,
+ rc::Rc,
+ sync::{Arc, Weak},
+ time::Duration,
+};
+
+const WINDOW_STATE_IVAR: &str = "windowState";
+
+static mut WINDOW_CLASS: *const Class = ptr::null();
+static mut PANEL_CLASS: *const Class = ptr::null();
+static mut VIEW_CLASS: *const Class = ptr::null();
+
+#[allow(non_upper_case_globals)]
+const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
+ unsafe { NSWindowStyleMask::from_bits_unchecked(1 << 7) };
+#[allow(non_upper_case_globals)]
+const NSNormalWindowLevel: NSInteger = 0;
+#[allow(non_upper_case_globals)]
+const NSPopUpWindowLevel: NSInteger = 101;
+#[allow(non_upper_case_globals)]
+const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
+#[allow(non_upper_case_globals)]
+const NSTrackingMouseMoved: NSUInteger = 0x02;
+#[allow(non_upper_case_globals)]
+const NSTrackingActiveAlways: NSUInteger = 0x80;
+#[allow(non_upper_case_globals)]
+const NSTrackingInVisibleRect: NSUInteger = 0x200;
+#[allow(non_upper_case_globals)]
+const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
+#[allow(non_upper_case_globals)]
+const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+
+#[ctor]
+unsafe fn build_classes() {
+ WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
+ PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
+ VIEW_CLASS = {
+ let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
+ decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+
+ decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
+
+ decl.add_method(
+ sel!(performKeyEquivalent:),
+ handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL,
+ );
+ decl.add_method(
+ sel!(keyDown:),
+ handle_key_down as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(mouseDown:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(mouseUp:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(rightMouseDown:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(rightMouseUp:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(otherMouseDown:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(otherMouseUp:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(mouseMoved:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(mouseExited:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(mouseDragged:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(scrollWheel:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(flagsChanged:),
+ handle_view_event as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(cancelOperation:),
+ cancel_operation as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(makeBackingLayer),
+ make_backing_layer as extern "C" fn(&Object, Sel) -> id,
+ );
+
+ decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
+ decl.add_method(
+ sel!(viewDidChangeBackingProperties),
+ view_did_change_backing_properties as extern "C" fn(&Object, Sel),
+ );
+ decl.add_method(
+ sel!(setFrameSize:),
+ set_frame_size as extern "C" fn(&Object, Sel, NSSize),
+ );
+ decl.add_method(
+ sel!(displayLayer:),
+ display_layer as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
+ decl.add_method(
+ sel!(validAttributesForMarkedText),
+ valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id,
+ );
+ decl.add_method(
+ sel!(hasMarkedText),
+ has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
+ );
+ decl.add_method(
+ sel!(markedRange),
+ marked_range as extern "C" fn(&Object, Sel) -> NSRange,
+ );
+ decl.add_method(
+ sel!(selectedRange),
+ selected_range as extern "C" fn(&Object, Sel) -> NSRange,
+ );
+ decl.add_method(
+ sel!(firstRectForCharacterRange:actualRange:),
+ first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect,
+ );
+ decl.add_method(
+ sel!(insertText:replacementRange:),
+ insert_text as extern "C" fn(&Object, Sel, id, NSRange),
+ );
+ decl.add_method(
+ sel!(setMarkedText:selectedRange:replacementRange:),
+ set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
+ );
+ decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
+ decl.add_method(
+ sel!(attributedSubstringForProposedRange:actualRange:),
+ attributed_substring_for_proposed_range
+ as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
+ );
+ decl.add_method(
+ sel!(viewDidChangeEffectiveAppearance),
+ view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
+ );
+
+ // Suppress beep on keystrokes with modifier keys.
+ decl.add_method(
+ sel!(doCommandBySelector:),
+ do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
+ );
+
+ decl.add_method(
+ sel!(acceptsFirstMouse:),
+ accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
+ );
+
+ decl.register()
+ };
+}
+
+pub fn convert_mouse_position(position: NSPoint, window_height: Pixels) -> Point<Pixels> {
+ point(
+ px(position.x as f32),
+ // MacOS screen coordinates are relative to bottom left
+ window_height - px(position.y as f32),
+ )
+}
+
+unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
+ let mut decl = ClassDecl::new(name, superclass).unwrap();
+ decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+ decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
+ decl.add_method(
+ sel!(canBecomeMainWindow),
+ yes as extern "C" fn(&Object, Sel) -> BOOL,
+ );
+ decl.add_method(
+ sel!(canBecomeKeyWindow),
+ yes as extern "C" fn(&Object, Sel) -> BOOL,
+ );
+ decl.add_method(
+ sel!(windowDidResize:),
+ window_did_resize as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowWillEnterFullScreen:),
+ window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowWillExitFullScreen:),
+ window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowDidMove:),
+ window_did_move as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowDidBecomeKey:),
+ window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowDidResignKey:),
+ window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+ );
+ decl.add_method(
+ sel!(windowShouldClose:),
+ window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
+ );
+ decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
+ decl.register()
+}
+
+///Used to track what the IME does when we send it a keystroke.
+///This is only used to handle the case where the IME mysteriously
+///swallows certain keys.
+///
+///Basically a direct copy of the approach that WezTerm uses in:
+///github.com/wez/wezterm : d5755f3e : window/src/os/macos/window.rs
+enum ImeState {
+ Continue,
+ Acted,
+ None,
+}
+
+struct InsertText {
+ replacement_range: Option<Range<usize>>,
+ text: String,
+}
+
+struct MacWindowState {
+ handle: AnyWindowHandle,
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ native_window: id,
+ renderer: MetalRenderer,
+ scene_to_render: Option<Scene>,
+ kind: WindowKind,
+ event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
+ activate_callback: Option<Box<dyn FnMut(bool)>>,
+ resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
+ fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
+ moved_callback: Option<Box<dyn FnMut()>>,
+ should_close_callback: Option<Box<dyn FnMut() -> bool>>,
+ close_callback: Option<Box<dyn FnOnce()>>,
+ appearance_changed_callback: Option<Box<dyn FnMut()>>,
+ input_handler: Option<Box<dyn InputHandler>>,
+ pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
+ last_key_equivalent: Option<KeyDownEvent>,
+ synthetic_drag_counter: usize,
+ last_fresh_keydown: Option<Keystroke>,
+ traffic_light_position: Option<Point<Pixels>>,
+ previous_modifiers_changed_event: Option<Event>,
+ // State tracking what the IME did after the last request
+ ime_state: ImeState,
+ // Retains the last IME Text
+ ime_text: Option<String>,
+}
+
+impl MacWindowState {
+ fn move_traffic_light(&self) {
+ if let Some(traffic_light_position) = self.traffic_light_position {
+ let titlebar_height = self.titlebar_height();
+
+ unsafe {
+ let close_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowCloseButton
+ ];
+ let min_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
+ ];
+ let zoom_button: id = msg_send![
+ self.native_window,
+ standardWindowButton: NSWindowButton::NSWindowZoomButton
+ ];
+
+ let mut close_button_frame: CGRect = msg_send![close_button, frame];
+ let mut min_button_frame: CGRect = msg_send![min_button, frame];
+ let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
+ let mut origin = point(
+ traffic_light_position.x,
+ titlebar_height
+ - traffic_light_position.y
+ - px(close_button_frame.size.height as f32),
+ );
+ let button_spacing =
+ px((min_button_frame.origin.x - close_button_frame.origin.x) as f32);
+
+ close_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+ let _: () = msg_send![close_button, setFrame: close_button_frame];
+ origin.x += button_spacing;
+
+ min_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+ let _: () = msg_send![min_button, setFrame: min_button_frame];
+ origin.x += button_spacing;
+
+ zoom_button_frame.origin = CGPoint::new(origin.x.into(), origin.y.into());
+ let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
+ origin.x += button_spacing;
+ }
+ }
+ }
+
+ fn is_fullscreen(&self) -> bool {
+ unsafe {
+ let style_mask = self.native_window.styleMask();
+ style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask)
+ }
+ }
+
+ fn bounds(&self) -> WindowBounds {
+ unsafe {
+ if self.is_fullscreen() {
+ return WindowBounds::Fullscreen;
+ }
+
+ let frame = self.frame();
+ let screen_size = self.native_window.screen().visibleFrame().size();
+ if frame.size == screen_size {
+ WindowBounds::Maximized
+ } else {
+ WindowBounds::Fixed(frame)
+ }
+ }
+ }
+
+ fn frame(&self) -> Bounds<Pixels> {
+ unsafe {
+ let frame = NSWindow::frame(self.native_window);
+ MacScreen::screen_bounds_from_native(frame)
+ }
+ }
+
+ fn content_size(&self) -> Size<Pixels> {
+ let NSSize { width, height, .. } =
+ unsafe { NSView::frame(self.native_window.contentView()) }.size;
+ size(px(width as f32), px(height as f32))
+ }
+
+ fn scale_factor(&self) -> f32 {
+ get_scale_factor(self.native_window)
+ }
+
+ fn titlebar_height(&self) -> Pixels {
+ unsafe {
+ let frame = NSWindow::frame(self.native_window);
+ let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
+ px((frame.size.height - content_layout_rect.size.height) as f32)
+ }
+ }
+
+ fn to_screen_ns_point(&self, point: Point<Pixels>) -> NSPoint {
+ unsafe {
+ let point = NSPoint::new(
+ point.x.into(),
+ (self.content_size().height - point.y).into(),
+ );
+ msg_send![self.native_window, convertPointToScreen: point]
+ }
+ }
+}
+
+unsafe impl Send for MacWindowState {}
+
+pub struct MacWindow(Arc<Mutex<MacWindowState>>);
+
+impl MacWindow {
+ pub fn open(handle: AnyWindowHandle, options: WindowOptions, platform: &dyn Platform) -> Self {
+ unsafe {
+ let pool = NSAutoreleasePool::new(nil);
+
+ let mut style_mask;
+ if let Some(titlebar) = options.titlebar.as_ref() {
+ style_mask = NSWindowStyleMask::NSClosableWindowMask
+ | NSWindowStyleMask::NSMiniaturizableWindowMask
+ | NSWindowStyleMask::NSResizableWindowMask
+ | NSWindowStyleMask::NSTitledWindowMask;
+
+ if titlebar.appears_transparent {
+ style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+ }
+ } else {
+ style_mask = NSWindowStyleMask::NSTitledWindowMask
+ | NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+ }
+
+ let native_window: id = match options.kind {
+ WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
+ WindowKind::PopUp => {
+ style_mask |= NSWindowStyleMaskNonactivatingPanel;
+ msg_send![PANEL_CLASS, alloc]
+ }
+ };
+ let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_(
+ NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)),
+ style_mask,
+ NSBackingStoreBuffered,
+ NO,
+ options
+ .screen
+ .map(|screen| MacScreen::from_handle(screen).native_screen)
+ .unwrap_or(nil),
+ );
+ assert!(!native_window.is_null());
+
+ let screen = native_window.screen();
+ match options.bounds {
+ WindowBounds::Fullscreen => {
+ native_window.toggleFullScreen_(nil);
+ }
+ WindowBounds::Maximized => {
+ native_window.setFrame_display_(screen.visibleFrame(), YES);
+ }
+ WindowBounds::Fixed(bounds) => {
+ let bounds = MacScreen::screen_bounds_to_native(bounds);
+ let screen_bounds = screen.visibleFrame();
+ if bounds.intersects(screen_bounds) {
+ native_window.setFrame_display_(bounds, YES);
+ } else {
+ native_window.setFrame_display_(screen_bounds, YES);
+ }
+ }
+ }
+
+ let native_view: id = msg_send![VIEW_CLASS, alloc];
+ let native_view = NSView::init(native_view);
+
+ assert!(!native_view.is_null());
+
+ let window = Self(Arc::new(Mutex::new(MacWindowState {
+ handle,
+ dispatcher: platform.dispatcher(),
+ native_window,
+ renderer: MetalRenderer::new(true),
+ scene_to_render: None,
+ kind: options.kind,
+ event_callback: None,
+ activate_callback: None,
+ resize_callback: None,
+ fullscreen_callback: None,
+ moved_callback: None,
+ should_close_callback: None,
+ close_callback: None,
+ appearance_changed_callback: None,
+ input_handler: None,
+ pending_key_down: None,
+ last_key_equivalent: None,
+ synthetic_drag_counter: 0,
+ last_fresh_keydown: None,
+ traffic_light_position: options
+ .titlebar
+ .as_ref()
+ .and_then(|titlebar| titlebar.traffic_light_position),
+ previous_modifiers_changed_event: None,
+ ime_state: ImeState::None,
+ ime_text: None,
+ })));
+
+ (*native_window).set_ivar(
+ WINDOW_STATE_IVAR,
+ Arc::into_raw(window.0.clone()) as *const c_void,
+ );
+ native_window.setDelegate_(native_window);
+ (*native_view).set_ivar(
+ WINDOW_STATE_IVAR,
+ Arc::into_raw(window.0.clone()) as *const c_void,
+ );
+
+ if let Some(title) = options
+ .titlebar
+ .as_ref()
+ .and_then(|t| t.title.as_ref().map(AsRef::as_ref))
+ {
+ native_window.setTitle_(NSString::alloc(nil).init_str(title));
+ }
+
+ native_window.setMovable_(options.is_movable as BOOL);
+
+ if options
+ .titlebar
+ .map_or(true, |titlebar| titlebar.appears_transparent)
+ {
+ native_window.setTitlebarAppearsTransparent_(YES);
+ native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
+ }
+
+ native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
+ native_view.setWantsBestResolutionOpenGLSurface_(YES);
+
+ // From winit crate: On Mojave, views automatically become layer-backed shortly after
+ // being added to a native_window. Changing the layer-backedness of a view breaks the
+ // association between the view and its associated OpenGL context. To work around this,
+ // on we explicitly make the view layer-backed up front so that AppKit doesn't do it
+ // itself and break the association with its context.
+ native_view.setWantsLayer(YES);
+ let _: () = msg_send![
+ native_view,
+ setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
+ ];
+
+ native_window.setContentView_(native_view.autorelease());
+ native_window.makeFirstResponder_(native_view);
+
+ if options.center {
+ native_window.center();
+ }
+
+ match options.kind {
+ WindowKind::Normal => {
+ native_window.setLevel_(NSNormalWindowLevel);
+ native_window.setAcceptsMouseMovedEvents_(YES);
+ }
+ WindowKind::PopUp => {
+ // Use a tracking area to allow receiving MouseMoved events even when
+ // the window or application aren't active, which is often the case
+ // e.g. for notification windows.
+ let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
+ let _: () = msg_send![
+ tracking_area,
+ initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
+ options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
+ owner: native_view
+ userInfo: nil
+ ];
+ let _: () =
+ msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
+
+ native_window.setLevel_(NSPopUpWindowLevel);
+ let _: () = msg_send![
+ native_window,
+ setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
+ ];
+ native_window.setCollectionBehavior_(
+ NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
+ NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
+ );
+ }
+ }
+ if options.focus {
+ native_window.makeKeyAndOrderFront_(nil);
+ } else if options.show {
+ native_window.orderFront_(nil);
+ }
+
+ window.0.lock().move_traffic_light();
+ pool.drain();
+
+ window
+ }
+ }
+
+ pub fn main_window() -> Option<AnyWindowHandle> {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let main_window: id = msg_send![app, mainWindow];
+ if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
+ let handle = get_window_state(&*main_window).lock().handle;
+ Some(handle)
+ } else {
+ None
+ }
+ }
+ }
+}
+
+impl Drop for MacWindow {
+ fn drop(&mut self) {
+ let this = self.0.clone();
+ let dispatcher = self.0.lock().dispatcher.clone();
+ let _ = crate::spawn_on_main(dispatcher, || async move {
+ unsafe {
+ this.lock().native_window.close();
+ }
+ });
+ }
+}
+
+impl PlatformWindow for MacWindow {
+ fn bounds(&self) -> WindowBounds {
+ self.0.as_ref().lock().bounds()
+ }
+
+ fn content_size(&self) -> Size<Pixels> {
+ self.0.as_ref().lock().content_size().into()
+ }
+
+ fn scale_factor(&self) -> f32 {
+ self.0.as_ref().lock().scale_factor()
+ }
+
+ fn titlebar_height(&self) -> Pixels {
+ self.0.as_ref().lock().titlebar_height()
+ }
+
+ fn appearance(&self) -> WindowAppearance {
+ unsafe {
+ let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance];
+ WindowAppearance::from_native(appearance)
+ }
+ }
+
+ fn screen(&self) -> Rc<dyn PlatformScreen> {
+ unsafe {
+ Rc::new(MacScreen {
+ native_screen: self.0.as_ref().lock().native_window.screen(),
+ })
+ }
+ }
+
+ fn mouse_position(&self) -> Point<Pixels> {
+ let position = unsafe {
+ self.0
+ .lock()
+ .native_window
+ .mouseLocationOutsideOfEventStream()
+ };
+ convert_mouse_position(position, self.content_size().height)
+ }
+
+ fn as_any_mut(&mut self) -> &mut dyn Any {
+ self
+ }
+
+ fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) {
+ self.0.as_ref().lock().input_handler = Some(input_handler);
+ }
+
+ fn prompt(
+ &self,
+ level: WindowPromptLevel,
+ msg: &str,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize> {
+ // macOs applies overrides to modal window buttons after they are added.
+ // Two most important for this logic are:
+ // * Buttons with "Cancel" title will be displayed as the last buttons in the modal
+ // * Last button added to the modal via `addButtonWithTitle` stays focused
+ // * Focused buttons react on "space"/" " keypresses
+ // * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus
+ //
+ // See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion
+ // ```
+ // By default, the first button has a key equivalent of Return,
+ // any button with a title of “Cancel” has a key equivalent of Escape,
+ // and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button).
+ // ```
+ //
+ // To avoid situations when the last element added is "Cancel" and it gets the focus
+ // (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button
+ // last, so it gets focus and a Space shortcut.
+ // This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key.
+ let latest_non_cancel_label = answers
+ .iter()
+ .enumerate()
+ .rev()
+ .find(|(_, &label)| label != "Cancel")
+ .filter(|&(label_index, _)| label_index > 0);
+
+ unsafe {
+ let alert: id = msg_send![class!(NSAlert), alloc];
+ let alert: id = msg_send![alert, init];
+ let alert_style = match level {
+ WindowPromptLevel::Info => 1,
+ WindowPromptLevel::Warning => 0,
+ WindowPromptLevel::Critical => 2,
+ };
+ let _: () = msg_send![alert, setAlertStyle: alert_style];
+ let _: () = msg_send![alert, setMessageText: ns_string(msg)];
+
+ for (ix, answer) in answers
+ .iter()
+ .enumerate()
+ .filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
+ {
+ let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+ let _: () = msg_send![button, setTag: ix as NSInteger];
+ }
+ if let Some((ix, answer)) = latest_non_cancel_label {
+ let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
+ let _: () = msg_send![button, setTag: ix as NSInteger];
+ }
+
+ let (done_tx, done_rx) = oneshot::channel();
+ let done_tx = Cell::new(Some(done_tx));
+ let block = ConcreteBlock::new(move |answer: NSInteger| {
+ if let Some(done_tx) = done_tx.take() {
+ let _ = done_tx.send(answer.try_into().unwrap());
+ }
+ });
+ let block = block.copy();
+ let native_window = self.0.lock().native_window;
+ let dispatcher = self.0.lock().dispatcher.clone();
+ let _ = crate::spawn_on_main_local(dispatcher, async move {
+ let _: () = msg_send![
+ alert,
+ beginSheetModalForWindow: native_window
+ completionHandler: block
+ ];
+ });
+
+ done_rx
+ }
+ }
+
+ fn activate(&self) {
+ let window = self.0.lock().native_window;
+ let dispatcher = self.0.lock().dispatcher.clone();
+ let _ = crate::spawn_on_main_local(dispatcher.clone(), async move {
+ unsafe {
+ let _: () = msg_send![window, makeKeyAndOrderFront: nil];
+ }
+ });
+ }
+
+ fn set_title(&mut self, title: &str) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let window = self.0.lock().native_window;
+ let title = ns_string(title);
+ let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
+ let _: () = msg_send![window, setTitle: title];
+ self.0.lock().move_traffic_light();
+ }
+ }
+
+ fn set_edited(&mut self, edited: bool) {
+ unsafe {
+ let window = self.0.lock().native_window;
+ msg_send![window, setDocumentEdited: edited as BOOL]
+ }
+
+ // Changing the document edited state resets the traffic light position,
+ // so we have to move it again.
+ self.0.lock().move_traffic_light();
+ }
+
+ fn show_character_palette(&self) {
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+ let window = self.0.lock().native_window;
+ let _: () = msg_send![app, orderFrontCharacterPalette: window];
+ }
+ }
+
+ fn minimize(&self) {
+ let window = self.0.lock().native_window;
+ unsafe {
+ window.miniaturize_(nil);
+ }
+ }
+
+ fn zoom(&self) {
+ let this = self.0.lock();
+ let window = this.native_window;
+ let dispatcher = this.dispatcher.clone();
+ let _ = crate::spawn_on_main_local(dispatcher, async move {
+ unsafe {
+ window.zoom_(nil);
+ }
+ });
+ }
+
+ fn toggle_full_screen(&self) {
+ let this = self.0.lock();
+ let window = this.native_window;
+ let dispatcher = this.dispatcher.clone();
+ let _ = crate::spawn_on_main_local(dispatcher, async move {
+ unsafe {
+ window.toggleFullScreen_(nil);
+ }
+ });
+ }
+
+ fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>) {
+ self.0.as_ref().lock().event_callback = Some(callback);
+ }
+
+ fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
+ self.0.as_ref().lock().activate_callback = Some(callback);
+ }
+
+ fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
+ self.0.as_ref().lock().resize_callback = Some(callback);
+ }
+
+ fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
+ self.0.as_ref().lock().fullscreen_callback = Some(callback);
+ }
+
+ fn on_moved(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().moved_callback = Some(callback);
+ }
+
+ fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
+ self.0.as_ref().lock().should_close_callback = Some(callback);
+ }
+
+ fn on_close(&self, callback: Box<dyn FnOnce()>) {
+ self.0.as_ref().lock().close_callback = Some(callback);
+ }
+
+ fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
+ self.0.lock().appearance_changed_callback = Some(callback);
+ }
+
+ fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool {
+ let self_borrow = self.0.lock();
+ let self_handle = self_borrow.handle;
+
+ unsafe {
+ let app = NSApplication::sharedApplication(nil);
+
+ // Convert back to screen coordinates
+ let screen_point = self_borrow.to_screen_ns_point(position);
+
+ let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
+ let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
+
+ let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
+ let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
+ if is_panel == YES || is_window == YES {
+ let topmost_window = get_window_state(&*top_most_window).lock().handle;
+ topmost_window == self_handle
+ } else {
+ // Someone else's window is on top
+ false
+ }
+ }
+ }
+
+ fn draw(&self, scene: crate::Scene) {
+ let mut this = self.0.lock();
+ this.scene_to_render = Some(scene);
+ unsafe {
+ let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
+ }
+ }
+}
+
+fn get_scale_factor(native_window: id) -> f32 {
+ unsafe {
+ let screen: id = msg_send![native_window, screen];
+ NSScreen::backingScaleFactor(screen) as f32
+ }
+}
+
+unsafe fn get_window_state(object: &Object) -> Arc<Mutex<MacWindowState>> {
+ let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
+ let rc1 = Arc::from_raw(raw as *mut Mutex<MacWindowState>);
+ let rc2 = rc1.clone();
+ mem::forget(rc1);
+ rc2
+}
+
+unsafe fn drop_window_state(object: &Object) {
+ let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
+ Rc::from_raw(raw as *mut RefCell<MacWindowState>);
+}
+
+extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
+ YES
+}
+
+extern "C" fn dealloc_window(this: &Object, _: Sel) {
+ unsafe {
+ drop_window_state(this);
+ let _: () = msg_send![super(this, class!(NSWindow)), dealloc];
+ }
+}
+
+extern "C" fn dealloc_view(this: &Object, _: Sel) {
+ unsafe {
+ drop_window_state(this);
+ let _: () = msg_send![super(this, class!(NSView)), dealloc];
+ }
+}
+
+extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
+ handle_key_event(this, native_event, true)
+}
+
+extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
+ handle_key_event(this, native_event, false);
+}
+
+extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+
+ let window_height = lock.content_size().height;
+ let event = unsafe { Event::from_native(native_event, Some(window_height)) };
+
+ if let Some(Event::KeyDown(event)) = event {
+ // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
+ // If that event isn't handled, it will then dispatch a "key down" event. GPUI
+ // makes no distinction between these two types of events, so we need to ignore
+ // the "key down" event if we've already just processed its "key equivalent" version.
+ if key_equivalent {
+ lock.last_key_equivalent = Some(event.clone());
+ } else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
+ return NO;
+ }
+
+ let keydown = event.keystroke.clone();
+ let fn_modifier = keydown.modifiers.function;
+ // Ignore events from held-down keys after some of the initially-pressed keys
+ // were released.
+ if event.is_held {
+ if lock.last_fresh_keydown.as_ref() != Some(&keydown) {
+ return YES;
+ }
+ } else {
+ lock.last_fresh_keydown = Some(keydown);
+ }
+ lock.pending_key_down = Some((event, None));
+ drop(lock);
+
+ // Send the event to the input context for IME handling, unless the `fn` modifier is
+ // being pressed.
+ if !fn_modifier {
+ unsafe {
+ let input_context: id = msg_send![this, inputContext];
+ let _: BOOL = msg_send![input_context, handleEvent: native_event];
+ }
+ }
+
+ let mut handled = false;
+ let mut lock = window_state.lock();
+ let ime_text = lock.ime_text.clone();
+ if let Some((event, insert_text)) = lock.pending_key_down.take() {
+ let is_held = event.is_held;
+ if let Some(mut callback) = lock.event_callback.take() {
+ drop(lock);
+
+ let is_composing =
+ with_input_handler(this, |input_handler| input_handler.marked_text_range())
+ .flatten()
+ .is_some();
+ if !is_composing {
+ // if the IME has changed the key, we'll first emit an event with the character
+ // generated by the IME system; then fallback to the keystroke if that is not
+ // handled.
+ // cases that we have working:
+ // - " on a brazillian layout by typing <quote><space>
+ // - ctrl-` on a brazillian layout by typing <ctrl-`>
+ // - $ on a czech QWERTY layout by typing <alt-4>
+ // - 4 on a czech QWERTY layout by typing <shift-4>
+ // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
+ if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
+ let event_with_ime_text = KeyDownEvent {
+ is_held: false,
+ keystroke: Keystroke {
+ // we match ctrl because some use-cases need it.
+ // we don't match alt because it's often used to generate the optional character
+ // we don't match shift because we're not here with letters (usually)
+ // we don't match cmd/fn because they don't seem to use IME
+ modifiers: Default::default(),
+ key: ime_text.clone().unwrap(),
+ },
+ };
+ handled = callback(Event::KeyDown(event_with_ime_text));
+ }
+ if !handled {
+ // empty key happens when you type a deadkey in input composition.
+ // (e.g. on a brazillian keyboard typing quote is a deadkey)
+ if !event.keystroke.key.is_empty() {
+ handled = callback(Event::KeyDown(event));
+ }
+ }
+ }
+
+ if !handled {
+ if let Some(insert) = insert_text {
+ handled = true;
+ with_input_handler(this, |input_handler| {
+ input_handler
+ .replace_text_in_range(insert.replacement_range, &insert.text)
+ });
+ } else if !is_composing && is_held {
+ if let Some(last_insert_text) = ime_text {
+ //MacOS IME is a bit funky, and even when you've told it there's nothing to
+ //inter it will still swallow certain keys (e.g. 'f', 'j') and not others
+ //(e.g. 'n'). This is a problem for certain kinds of views, like the terminal
+ with_input_handler(this, |input_handler| {
+ if input_handler.selected_text_range().is_none() {
+ handled = true;
+ input_handler.replace_text_in_range(None, &last_insert_text)
+ }
+ });
+ }
+ }
+ }
+
+ window_state.lock().event_callback = Some(callback);
+ }
+ } else {
+ handled = true;
+ }
+
+ handled as BOOL
+ } else {
+ NO
+ }
+}
+
+extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let weak_window_state = Arc::downgrade(&window_state);
+ let mut lock = window_state.as_ref().lock();
+ let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
+
+ let window_height = lock.content_size().height;
+ let event = unsafe { Event::from_native(native_event, Some(window_height)) };
+
+ if let Some(mut event) = event {
+ let synthesized_second_event = match &mut event {
+ Event::MouseDown(
+ event @ MouseDownEvent {
+ button: MouseButton::Left,
+ modifiers: Modifiers { control: true, .. },
+ ..
+ },
+ ) => {
+ *event = MouseDownEvent {
+ button: MouseButton::Right,
+ modifiers: Modifiers {
+ control: false,
+ ..event.modifiers
+ },
+ click_count: 1,
+ ..*event
+ };
+
+ Some(Event::MouseDown(MouseDownEvent {
+ button: MouseButton::Right,
+ ..*event
+ }))
+ }
+
+ // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
+ // the ctrl-left_up to avoid having a mismatch in button down/up events if the
+ // user is still holding ctrl when releasing the left mouse button
+ Event::MouseUp(MouseUpEvent {
+ button: MouseButton::Left,
+ modifiers: Modifiers { control: true, .. },
+ ..
+ }) => {
+ lock.synthetic_drag_counter += 1;
+ return;
+ }
+
+ _ => None,
+ };
+
+ match &event {
+ Event::MouseMoved(
+ event @ MouseMovedEvent {
+ pressed_button: Some(_),
+ ..
+ },
+ ) => {
+ lock.synthetic_drag_counter += 1;
+ let dispatcher = lock.dispatcher.clone();
+ let _ = crate::spawn_on_main_local(
+ dispatcher,
+ synthetic_drag(
+ weak_window_state,
+ lock.synthetic_drag_counter,
+ event.clone(),
+ ),
+ );
+ }
+
+ Event::MouseMoved(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
+
+ Event::MouseUp(MouseUpEvent {
+ button: MouseButton::Left,
+ ..
+ }) => {
+ lock.synthetic_drag_counter += 1;
+ }
+
+ Event::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
+ // Only raise modifiers changed event when they have actually changed
+ if let Some(Event::ModifiersChanged(ModifiersChangedEvent {
+ modifiers: prev_modifiers,
+ })) = &lock.previous_modifiers_changed_event
+ {
+ if prev_modifiers == modifiers {
+ return;
+ }
+ }
+
+ lock.previous_modifiers_changed_event = Some(event.clone());
+ }
+
+ _ => {}
+ }
+
+ if let Some(mut callback) = lock.event_callback.take() {
+ drop(lock);
+ callback(event);
+ if let Some(event) = synthesized_second_event {
+ callback(event);
+ }
+ window_state.lock().event_callback = Some(callback);
+ }
+ }
+}
+
+// Allows us to receive `cmd-.` (the shortcut for closing a dialog)
+// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6
+extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+
+ let keystroke = Keystroke {
+ modifiers: Default::default(),
+ key: ".".into(),
+ };
+ let event = Event::KeyDown(KeyDownEvent {
+ keystroke: keystroke.clone(),
+ is_held: false,
+ });
+
+ lock.last_fresh_keydown = Some(keystroke);
+ if let Some(mut callback) = lock.event_callback.take() {
+ drop(lock);
+ callback(event);
+ window_state.lock().event_callback = Some(callback);
+ }
+}
+
+extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
+ let window_state = unsafe { get_window_state(this) };
+ window_state.as_ref().lock().move_traffic_light();
+}
+
+extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
+ window_fullscreen_changed(this, true);
+}
+
+extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
+ window_fullscreen_changed(this, false);
+}
+
+fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.fullscreen_callback.take() {
+ drop(lock);
+ callback(is_fullscreen);
+ window_state.lock().fullscreen_callback = Some(callback);
+ }
+}
+
+extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.moved_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().moved_callback = Some(callback);
+ }
+}
+
+extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let lock = window_state.lock();
+ let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
+
+ // When opening a pop-up while the application isn't active, Cocoa sends a spurious
+ // `windowDidBecomeKey` message to the previous key window even though that window
+ // isn't actually key. This causes a bug if the application is later activated while
+ // the pop-up is still open, making it impossible to activate the previous key window
+ // even if the pop-up gets closed. The only way to activate it again is to de-activate
+ // the app and re-activate it, which is a pretty bad UX.
+ // The following code detects the spurious event and invokes `resignKeyWindow`:
+ // in theory, we're not supposed to invoke this method manually but it balances out
+ // the spurious `becomeKeyWindow` event and helps us work around that bug.
+ if selector == sel!(windowDidBecomeKey:) {
+ if !is_active {
+ unsafe {
+ let _: () = msg_send![lock.native_window, resignKeyWindow];
+ return;
+ }
+ }
+ }
+
+ let dispatcher = lock.dispatcher.clone();
+ drop(lock);
+ let _ = crate::spawn_on_main_local(dispatcher, async move {
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.activate_callback.take() {
+ drop(lock);
+ callback(is_active);
+ window_state.lock().activate_callback = Some(callback);
+ };
+ });
+}
+
+extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.should_close_callback.take() {
+ drop(lock);
+ let should_close = callback();
+ window_state.lock().should_close_callback = Some(callback);
+ should_close as BOOL
+ } else {
+ YES
+ }
+}
+
+extern "C" fn close_window(this: &Object, _: Sel) {
+ unsafe {
+ let close_callback = {
+ let window_state = get_window_state(this);
+ window_state
+ .as_ref()
+ .try_lock()
+ .and_then(|mut window_state| window_state.close_callback.take())
+ };
+
+ if let Some(callback) = close_callback {
+ callback();
+ }
+
+ let _: () = msg_send![super(this, class!(NSWindow)), close];
+ }
+}
+
+extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
+ let window_state = unsafe { get_window_state(this) };
+ let window_state = window_state.as_ref().lock();
+ window_state.renderer.layer().as_ptr() as id
+}
+
+extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+
+ unsafe {
+ let scale_factor = lock.scale_factor() as f64;
+ let size = lock.content_size();
+ let drawable_size: NSSize = NSSize {
+ width: f64::from(size.width) * scale_factor,
+ height: f64::from(size.height) * scale_factor,
+ };
+
+ let _: () = msg_send![
+ lock.renderer.layer(),
+ setContentsScale: scale_factor
+ ];
+ let _: () = msg_send![
+ lock.renderer.layer(),
+ setDrawableSize: drawable_size
+ ];
+ }
+
+ if let Some(mut callback) = lock.resize_callback.take() {
+ let content_size = lock.content_size();
+ let scale_factor = lock.scale_factor();
+ drop(lock);
+ callback(content_size, scale_factor);
+ window_state.as_ref().lock().resize_callback = Some(callback);
+ };
+}
+
+extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
+ let window_state = unsafe { get_window_state(this) };
+ let lock = window_state.as_ref().lock();
+
+ if lock.content_size() == size.into() {
+ return;
+ }
+
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size];
+ }
+
+ let scale_factor = lock.scale_factor() as f64;
+ let drawable_size: NSSize = NSSize {
+ width: size.width * scale_factor,
+ height: size.height * scale_factor,
+ };
+
+ unsafe {
+ let _: () = msg_send![
+ lock.renderer.layer(),
+ setDrawableSize: drawable_size
+ ];
+ }
+
+ drop(lock);
+ let mut lock = window_state.lock();
+ if let Some(mut callback) = lock.resize_callback.take() {
+ let content_size = lock.content_size();
+ let scale_factor = lock.scale_factor();
+ drop(lock);
+ callback(content_size, scale_factor);
+ window_state.lock().resize_callback = Some(callback);
+ };
+}
+
+extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
+ unsafe {
+ let window_state = get_window_state(this);
+ let mut window_state = window_state.as_ref().lock();
+ if let Some(scene) = window_state.scene_to_render.take() {
+ dbg!("render", &scene);
+ window_state.renderer.draw(&scene);
+ }
+ }
+}
+
+extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
+ unsafe { msg_send![class!(NSArray), array] }
+}
+
+extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
+ with_input_handler(this, |input_handler| input_handler.marked_text_range())
+ .flatten()
+ .is_some() as BOOL
+}
+
+extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
+ with_input_handler(this, |input_handler| input_handler.marked_text_range())
+ .flatten()
+ .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
+ with_input_handler(this, |input_handler| input_handler.selected_text_range())
+ .flatten()
+ .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn first_rect_for_character_range(
+ this: &Object,
+ _: Sel,
+ range: NSRange,
+ _: id,
+) -> NSRect {
+ let frame = unsafe {
+ let window = get_window_state(this).lock().native_window;
+ NSView::frame(window)
+ };
+ with_input_handler(this, |input_handler| {
+ input_handler.bounds_for_range(range.to_range()?)
+ })
+ .flatten()
+ .map_or(
+ NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)),
+ |bounds| {
+ NSRect::new(
+ NSPoint::new(
+ frame.origin.x + bounds.origin.x as f64,
+ frame.origin.y + frame.size.height - bounds.origin.y as f64,
+ ),
+ NSSize::new(bounds.size.width as f64, bounds.size.height as f64),
+ )
+ },
+ )
+}
+
+extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
+ unsafe {
+ let window_state = get_window_state(this);
+ let mut lock = window_state.lock();
+ let pending_key_down = lock.pending_key_down.take();
+ drop(lock);
+
+ let is_attributed_string: BOOL =
+ msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+ let text: id = if is_attributed_string == YES {
+ msg_send![text, string]
+ } else {
+ text
+ };
+ let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+ .to_str()
+ .unwrap();
+ let replacement_range = replacement_range.to_range();
+
+ window_state.lock().ime_text = Some(text.to_string());
+ window_state.lock().ime_state = ImeState::Acted;
+
+ let is_composing =
+ with_input_handler(this, |input_handler| input_handler.marked_text_range())
+ .flatten()
+ .is_some();
+
+ if is_composing || text.chars().count() > 1 || pending_key_down.is_none() {
+ with_input_handler(this, |input_handler| {
+ input_handler.replace_text_in_range(replacement_range, text)
+ });
+ } else {
+ let mut pending_key_down = pending_key_down.unwrap();
+ pending_key_down.1 = Some(InsertText {
+ replacement_range,
+ text: text.to_string(),
+ });
+ window_state.lock().pending_key_down = Some(pending_key_down);
+ }
+ }
+}
+
+extern "C" fn set_marked_text(
+ this: &Object,
+ _: Sel,
+ text: id,
+ selected_range: NSRange,
+ replacement_range: NSRange,
+) {
+ unsafe {
+ let window_state = get_window_state(this);
+ window_state.lock().pending_key_down.take();
+
+ let is_attributed_string: BOOL =
+ msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+ let text: id = if is_attributed_string == YES {
+ msg_send![text, string]
+ } else {
+ text
+ };
+ let selected_range = selected_range.to_range();
+ let replacement_range = replacement_range.to_range();
+ let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+ .to_str()
+ .unwrap();
+
+ window_state.lock().ime_state = ImeState::Acted;
+ window_state.lock().ime_text = Some(text.to_string());
+
+ with_input_handler(this, |input_handler| {
+ input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range);
+ });
+ }
+}
+
+extern "C" fn unmark_text(this: &Object, _: Sel) {
+ unsafe {
+ let state = get_window_state(this);
+ let mut borrow = state.lock();
+ borrow.ime_state = ImeState::Acted;
+ borrow.ime_text.take();
+ }
+
+ with_input_handler(this, |input_handler| input_handler.unmark_text());
+}
+
+extern "C" fn attributed_substring_for_proposed_range(
+ this: &Object,
+ _: Sel,
+ range: NSRange,
+ _actual_range: *mut c_void,
+) -> id {
+ with_input_handler(this, |input_handler| {
+ let range = range.to_range()?;
+ if range.is_empty() {
+ return None;
+ }
+
+ let selected_text = input_handler.text_for_range(range)?;
+ unsafe {
+ let string: id = msg_send![class!(NSAttributedString), alloc];
+ let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
+ Some(string)
+ }
+ })
+ .flatten()
+ .unwrap_or(nil)
+}
+
+extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
+ unsafe {
+ let state = get_window_state(this);
+ let mut borrow = state.lock();
+ borrow.ime_state = ImeState::Continue;
+ borrow.ime_text.take();
+ }
+}
+
+extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
+ unsafe {
+ let state = get_window_state(this);
+ let mut lock = state.as_ref().lock();
+ if let Some(mut callback) = lock.appearance_changed_callback.take() {
+ drop(lock);
+ callback();
+ state.lock().appearance_changed_callback = Some(callback);
+ }
+ }
+}
+
+extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
+ unsafe {
+ let state = get_window_state(this);
+ let lock = state.as_ref().lock();
+ return if lock.kind == WindowKind::PopUp {
+ YES
+ } else {
+ NO
+ };
+ }
+}
+
+async fn synthetic_drag(
+ window_state: Weak<Mutex<MacWindowState>>,
+ drag_id: usize,
+ event: MouseMovedEvent,
+) {
+ loop {
+ Timer::after(Duration::from_millis(16)).await;
+ if let Some(window_state) = window_state.upgrade() {
+ let mut lock = window_state.lock();
+ if lock.synthetic_drag_counter == drag_id {
+ if let Some(mut callback) = lock.event_callback.take() {
+ drop(lock);
+ callback(Event::MouseMoved(event.clone()));
+ window_state.lock().event_callback = Some(callback);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+}
+
+fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
+where
+ F: FnOnce(&mut dyn InputHandler) -> R,
+{
+ let window_state = unsafe { get_window_state(window) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut input_handler) = lock.input_handler.take() {
+ drop(lock);
+ let result = f(input_handler.as_mut());
+ window_state.lock().input_handler = Some(input_handler);
+ Some(result)
+ } else {
+ None
+ }
+}
@@ -0,0 +1,35 @@
+use crate::WindowAppearance;
+use cocoa::{
+ appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
+ base::id,
+ foundation::NSString,
+};
+use objc::{msg_send, sel, sel_impl};
+use std::ffi::CStr;
+
+impl WindowAppearance {
+ pub unsafe fn from_native(appearance: id) -> Self {
+ let name: id = msg_send![appearance, name];
+ if name == NSAppearanceNameVibrantLight {
+ Self::VibrantLight
+ } else if name == NSAppearanceNameVibrantDark {
+ Self::VibrantDark
+ } else if name == NSAppearanceNameAqua {
+ Self::Light
+ } else if name == NSAppearanceNameDarkAqua {
+ Self::Dark
+ } else {
+ println!(
+ "unknown appearance: {:?}",
+ CStr::from_ptr(name.UTF8String())
+ );
+ Self::Light
+ }
+ }
+}
+
+#[link(name = "AppKit", kind = "framework")]
+extern "C" {
+ pub static NSAppearanceNameAqua: id;
+ pub static NSAppearanceNameDarkAqua: id;
+}
@@ -0,0 +1,172 @@
+use super::Platform;
+use crate::ScreenId;
+
+pub struct TestPlatform;
+
+impl TestPlatform {
+ pub fn new() -> Self {
+ TestPlatform
+ }
+}
+
+// todo!("implement out what our tests needed in GPUI 1")
+impl Platform for TestPlatform {
+ fn dispatcher(&self) -> std::sync::Arc<dyn crate::PlatformDispatcher> {
+ unimplemented!()
+ }
+
+ fn text_system(&self) -> std::sync::Arc<dyn crate::PlatformTextSystem> {
+ unimplemented!()
+ }
+
+ fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
+ unimplemented!()
+ }
+
+ fn quit(&self) {
+ unimplemented!()
+ }
+
+ fn restart(&self) {
+ unimplemented!()
+ }
+
+ fn activate(&self, _ignoring_other_apps: bool) {
+ unimplemented!()
+ }
+
+ fn hide(&self) {
+ unimplemented!()
+ }
+
+ fn hide_other_apps(&self) {
+ unimplemented!()
+ }
+
+ fn unhide_other_apps(&self) {
+ unimplemented!()
+ }
+
+ fn screens(&self) -> Vec<std::rc::Rc<dyn crate::PlatformScreen>> {
+ unimplemented!()
+ }
+
+ fn screen_by_id(&self, _id: ScreenId) -> Option<std::rc::Rc<dyn crate::PlatformScreen>> {
+ unimplemented!()
+ }
+
+ fn main_window(&self) -> Option<crate::AnyWindowHandle> {
+ unimplemented!()
+ }
+
+ fn open_window(
+ &self,
+ _handle: crate::AnyWindowHandle,
+ _options: crate::WindowOptions,
+ ) -> Box<dyn crate::PlatformWindow> {
+ unimplemented!()
+ }
+
+ fn open_url(&self, _url: &str) {
+ unimplemented!()
+ }
+
+ fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
+ unimplemented!()
+ }
+
+ fn prompt_for_paths(
+ &self,
+ _options: crate::PathPromptOptions,
+ ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+ unimplemented!()
+ }
+
+ fn prompt_for_new_path(
+ &self,
+ _directory: &std::path::Path,
+ ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
+ unimplemented!()
+ }
+
+ fn reveal_path(&self, _path: &std::path::Path) {
+ unimplemented!()
+ }
+
+ fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
+ unimplemented!()
+ }
+
+ fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
+ unimplemented!()
+ }
+
+ fn on_quit(&self, _callback: Box<dyn FnMut()>) {
+ unimplemented!()
+ }
+
+ fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
+ unimplemented!()
+ }
+
+ fn on_event(&self, _callback: Box<dyn FnMut(crate::Event) -> bool>) {
+ unimplemented!()
+ }
+
+ fn os_name(&self) -> &'static str {
+ unimplemented!()
+ }
+
+ fn os_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+ unimplemented!()
+ }
+
+ fn app_version(&self) -> anyhow::Result<crate::SemanticVersion> {
+ unimplemented!()
+ }
+
+ fn app_path(&self) -> anyhow::Result<std::path::PathBuf> {
+ unimplemented!()
+ }
+
+ fn local_timezone(&self) -> time::UtcOffset {
+ unimplemented!()
+ }
+
+ fn path_for_auxiliary_executable(&self, _name: &str) -> anyhow::Result<std::path::PathBuf> {
+ unimplemented!()
+ }
+
+ fn set_cursor_style(&self, _style: crate::CursorStyle) {
+ unimplemented!()
+ }
+
+ fn should_auto_hide_scrollbars(&self) -> bool {
+ unimplemented!()
+ }
+
+ fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
+ unimplemented!()
+ }
+
+ fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
+ unimplemented!()
+ }
+
+ fn write_credentials(
+ &self,
+ _url: &str,
+ _username: &str,
+ _password: &[u8],
+ ) -> anyhow::Result<()> {
+ unimplemented!()
+ }
+
+ fn read_credentials(&self, _url: &str) -> anyhow::Result<Option<(String, Vec<u8>)>> {
+ unimplemented!()
+ }
+
+ fn delete_credentials(&self, _url: &str) -> anyhow::Result<()> {
+ unimplemented!()
+ }
+}
@@ -0,0 +1,121 @@
+use std::mem;
+
+use super::{Bounds, Hsla, Pixels, Point};
+use crate::{Corners, Edges};
+use bytemuck::{Pod, Zeroable};
+use collections::BTreeMap;
+
+// Exported to metal
+pub type PointF = Point<f32>;
+
+#[derive(Debug)]
+pub struct Scene {
+ layers: BTreeMap<u32, SceneLayer>,
+ pub(crate) scale_factor: f32,
+}
+
+#[derive(Default, Debug)]
+pub struct SceneLayer {
+ pub quads: Vec<Quad>,
+}
+
+impl Scene {
+ pub fn new(scale_factor: f32) -> Scene {
+ Scene {
+ layers: Default::default(),
+ scale_factor,
+ }
+ }
+
+ pub fn take(&mut self) -> Scene {
+ Scene {
+ layers: mem::take(&mut self.layers),
+ scale_factor: self.scale_factor,
+ }
+ }
+
+ pub fn insert(&mut self, primitive: impl Into<Primitive>) {
+ let mut primitive = primitive.into();
+ primitive.scale(self.scale_factor);
+ let layer = self.layers.entry(primitive.order()).or_default();
+ match primitive {
+ Primitive::Quad(quad) => layer.quads.push(quad),
+ }
+ }
+
+ pub fn layers(&self) -> impl Iterator<Item = &SceneLayer> {
+ self.layers.values()
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum Primitive {
+ Quad(Quad),
+}
+
+impl Primitive {
+ pub fn order(&self) -> u32 {
+ match self {
+ Primitive::Quad(quad) => quad.order,
+ }
+ }
+
+ pub fn is_transparent(&self) -> bool {
+ match self {
+ Primitive::Quad(quad) => {
+ quad.background.is_transparent() && quad.border_color.is_transparent()
+ }
+ }
+ }
+
+ pub fn scale(&mut self, factor: f32) {
+ match self {
+ Primitive::Quad(quad) => {
+ quad.scale(factor);
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
+#[repr(C)]
+pub struct Quad {
+ pub order: u32,
+ pub bounds: Bounds<Pixels>,
+ pub clip_bounds: Bounds<Pixels>,
+ pub clip_corner_radii: Corners<Pixels>,
+ pub background: Hsla,
+ pub border_color: Hsla,
+ pub corner_radii: Corners<Pixels>,
+ pub border_widths: Edges<Pixels>,
+}
+
+impl Quad {
+ pub fn vertices(&self) -> impl Iterator<Item = Point<Pixels>> {
+ let x1 = self.bounds.origin.x;
+ let y1 = self.bounds.origin.y;
+ let x2 = x1 + self.bounds.size.width;
+ let y2 = y1 + self.bounds.size.height;
+ [
+ Point::new(x1, y1),
+ Point::new(x2, y1),
+ Point::new(x2, y2),
+ Point::new(x1, y2),
+ ]
+ .into_iter()
+ }
+
+ pub fn scale(&mut self, factor: f32) {
+ self.bounds *= factor;
+ self.clip_bounds *= factor;
+ self.clip_corner_radii *= factor;
+ self.corner_radii *= factor;
+ self.border_widths *= factor;
+ }
+}
+
+impl From<Quad> for Primitive {
+ fn from(quad: Quad) -> Self {
+ Primitive::Quad(quad)
+ }
+}
@@ -0,0 +1,338 @@
+use crate::{
+ phi, rems, AbsoluteLength, Bounds, Corners, CornersRefinement, DefiniteLength, Edges,
+ EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point,
+ PointRefinement, Quad, Rems, Result, RunStyle, SharedString, Size, SizeRefinement, ViewContext,
+ WindowContext,
+};
+use refineable::Refineable;
+pub use taffy::style::{
+ AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
+ Overflow, Position,
+};
+
+#[derive(Clone, Refineable, Debug)]
+#[refineable(debug)]
+pub struct Style {
+ /// What layout strategy should be used?
+ pub display: Display,
+
+ // Overflow properties
+ /// How children overflowing their container should affect layout
+ #[refineable]
+ pub overflow: Point<Overflow>,
+ /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
+ pub scrollbar_width: f32,
+
+ // 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<Length>,
+
+ // Size properies
+ /// Sets the initial size of the item
+ #[refineable]
+ pub size: Size<Length>,
+ /// Controls the minimum size of the item
+ #[refineable]
+ pub min_size: Size<Length>,
+ /// Controls the maximum size of the item
+ #[refineable]
+ pub max_size: Size<Length>,
+ /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
+ pub aspect_ratio: Option<f32>,
+
+ // Spacing Properties
+ /// How large should the margin be on each side?
+ #[refineable]
+ pub margin: Edges<Length>,
+ /// How large should the padding be on each side?
+ #[refineable]
+ pub padding: Edges<DefiniteLength>,
+ /// How large should the border be on each side?
+ #[refineable]
+ pub border_widths: Edges<AbsoluteLength>,
+
+ // Alignment properties
+ /// How this node's children aligned in the cross/block axis?
+ pub align_items: Option<AlignItems>,
+ /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
+ pub align_self: Option<AlignSelf>,
+ /// How should content contained within this item be aligned in the cross/block axis
+ pub align_content: Option<AlignContent>,
+ /// How should contained within this item be aligned in the main/inline axis
+ pub justify_content: Option<JustifyContent>,
+ /// How large should the gaps between items in a flex container be?
+ #[refineable]
+ pub gap: Size<DefiniteLength>,
+
+ // Flexbox properies
+ /// 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 fill: Option<Fill>,
+
+ /// The border color of this element
+ pub border_color: Option<Hsla>,
+
+ /// The radius of the corners of this element
+ #[refineable]
+ pub corner_radii: Corners<AbsoluteLength>,
+
+ /// TEXT
+ pub text: TextStyleRefinement,
+}
+
+#[derive(Refineable, Clone, Debug)]
+#[refineable(debug)]
+pub struct TextStyle {
+ pub color: Hsla,
+ pub font_family: SharedString,
+ pub font_features: FontFeatures,
+ pub font_size: Rems,
+ pub line_height: DefiniteLength,
+ pub font_weight: FontWeight,
+ pub font_style: FontStyle,
+ pub underline: Option<UnderlineStyle>,
+}
+
+impl Default for TextStyle {
+ fn default() -> Self {
+ TextStyle {
+ color: Hsla::default(),
+ font_family: SharedString::default(),
+ font_features: FontFeatures::default(),
+ font_size: rems(1.),
+ line_height: phi(),
+ font_weight: FontWeight::default(),
+ font_style: FontStyle::default(),
+ underline: None,
+ }
+ }
+}
+
+impl TextStyle {
+ pub fn highlight(mut self, style: HighlightStyle) -> Result<Self> {
+ 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(underline) = style.underline {
+ self.underline = Some(underline);
+ }
+
+ Ok(self)
+ }
+
+ pub fn to_run(&self) -> RunStyle {
+ RunStyle {
+ font: Font {
+ family: self.font_family.clone(),
+ features: Default::default(),
+ weight: self.font_weight,
+ style: self.font_style,
+ },
+ color: self.color,
+ underline: self.underline.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct HighlightStyle {
+ pub color: Option<Hsla>,
+ pub font_weight: Option<FontWeight>,
+ pub font_style: Option<FontStyle>,
+ pub underline: Option<UnderlineStyle>,
+ pub fade_out: Option<f32>,
+}
+
+impl Eq for HighlightStyle {}
+
+impl Style {
+ pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
+ if self.text.is_some() {
+ Some(&self.text)
+ } else {
+ None
+ }
+ }
+
+ /// Paints the background of an element styled with this style.
+ pub fn paint<V: 'static>(&self, order: u32, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
+ let rem_size = cx.rem_size();
+
+ let background_color = self.fill.as_ref().and_then(Fill::color);
+ if background_color.is_some() || self.is_border_visible() {
+ cx.scene().insert(Quad {
+ order,
+ bounds,
+ clip_bounds: bounds, // todo!
+ clip_corner_radii: self.corner_radii.map(|length| length.to_pixels(rem_size)),
+ background: background_color.unwrap_or_default(),
+ border_color: self.border_color.unwrap_or_default(),
+ corner_radii: self.corner_radii.map(|length| length.to_pixels(rem_size)),
+ border_widths: self.border_widths.map(|length| length.to_pixels(rem_size)),
+ });
+ }
+ }
+
+ fn is_border_visible(&self) -> bool {
+ self.border_color
+ .map_or(false, |color| !color.is_transparent())
+ && self.border_widths.any(|length| !length.is_zero())
+ }
+}
+
+impl Default for Style {
+ fn default() -> Self {
+ Style {
+ display: Display::Block,
+ overflow: Point {
+ x: Overflow::Visible,
+ y: Overflow::Visible,
+ },
+ scrollbar_width: 0.0,
+ position: Position::Relative,
+ inset: Edges::auto(),
+ margin: Edges::<Length>::zero(),
+ padding: Edges::<DefiniteLength>::zero(),
+ border_widths: Edges::<AbsoluteLength>::zero(),
+ size: Size::auto(),
+ min_size: Size::auto(),
+ max_size: Size::auto(),
+ aspect_ratio: None,
+ gap: Size::zero(),
+ // Aligment
+ 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,
+ fill: None,
+ border_color: None,
+ corner_radii: Corners::default(),
+ text: TextStyleRefinement::default(),
+ }
+ }
+}
+
+#[derive(Refineable, Clone, Default, Debug, PartialEq, Eq)]
+#[refineable(debug)]
+pub struct UnderlineStyle {
+ pub thickness: Pixels,
+ pub color: Option<Hsla>,
+ pub squiggly: bool,
+}
+
+#[derive(Clone, Debug)]
+pub enum Fill {
+ Color(Hsla),
+}
+
+impl Fill {
+ pub fn color(&self) -> Option<Hsla> {
+ match self {
+ Fill::Color(color) => Some(*color),
+ }
+ }
+}
+
+impl Default for Fill {
+ fn default() -> Self {
+ Self::Color(Hsla::default())
+ }
+}
+
+impl From<Hsla> for Fill {
+ fn from(color: Hsla) -> Self {
+ Self::Color(color)
+ }
+}
+
+impl From<TextStyle> 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),
+ underline: other.underline.clone(),
+ fade_out: None,
+ }
+ }
+}
+
+impl HighlightStyle {
+ pub fn highlight(&mut self, other: HighlightStyle) {
+ match (self.color, other.color) {
+ (Some(self_color), Some(other_color)) => {
+ self.color = Some(Hsla::blend(other_color, self_color));
+ }
+ (None, Some(other_color)) => {
+ self.color = Some(other_color);
+ }
+ _ => {}
+ }
+
+ if other.font_weight.is_some() {
+ self.font_weight = other.font_weight;
+ }
+
+ if other.font_style.is_some() {
+ self.font_style = other.font_style;
+ }
+
+ if other.underline.is_some() {
+ self.underline = other.underline;
+ }
+
+ match (other.fade_out, self.fade_out) {
+ (Some(source_fade), None) => self.fade_out = Some(source_fade),
+ (Some(source_fade), Some(dest_fade)) => {
+ self.fade_out = Some((dest_fade * (1. + source_fade)).clamp(0., 1.));
+ }
+ _ => {}
+ }
+ }
+}
+
+impl From<Hsla> for HighlightStyle {
+ fn from(color: Hsla) -> Self {
+ Self {
+ color: Some(color),
+ ..Default::default()
+ }
+ }
+}
@@ -0,0 +1,308 @@
+use crate::{
+ self as gpui3, relative, rems, AlignItems, Display, Fill, FlexDirection, Hsla, JustifyContent,
+ Length, Position, SharedString, Style, StyleRefinement, Styled, TextStyleRefinement,
+};
+
+pub trait StyleHelpers: Styled<Style = Style> {
+ gpui3_macros::style_helpers!();
+
+ fn h(mut self, height: Length) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.height = Some(height);
+ self
+ }
+
+ /// size_{n}: Sets width & height to {n}
+ ///
+ /// Example:
+ /// size_1: Sets width & height to 1
+ fn size(mut self, size: Length) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.height = Some(size);
+ self.declared_style().size.width = Some(size);
+ self
+ }
+
+ fn full(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().size.width = Some(relative(1.).into());
+ self.declared_style().size.height = Some(relative(1.).into());
+ self
+ }
+
+ fn relative(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().position = Some(Position::Relative);
+ self
+ }
+
+ fn absolute(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().position = Some(Position::Absolute);
+ self
+ }
+
+ fn block(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().display = Some(Display::Block);
+ self
+ }
+
+ fn flex(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().display = Some(Display::Flex);
+ self
+ }
+
+ fn flex_col(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_direction = Some(FlexDirection::Column);
+ self
+ }
+
+ fn flex_row(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_direction = Some(FlexDirection::Row);
+ self
+ }
+
+ fn flex_1(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_grow = Some(1.);
+ self.declared_style().flex_shrink = Some(1.);
+ self.declared_style().flex_basis = Some(relative(0.).into());
+ self
+ }
+
+ fn flex_auto(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_grow = Some(1.);
+ self.declared_style().flex_shrink = Some(1.);
+ self.declared_style().flex_basis = Some(Length::Auto);
+ self
+ }
+
+ fn flex_initial(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_grow = Some(0.);
+ self.declared_style().flex_shrink = Some(1.);
+ self.declared_style().flex_basis = Some(Length::Auto);
+ self
+ }
+
+ fn flex_none(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_grow = Some(0.);
+ self.declared_style().flex_shrink = Some(0.);
+ self
+ }
+
+ fn grow(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().flex_grow = Some(1.);
+ self
+ }
+
+ fn items_start(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().align_items = Some(AlignItems::FlexStart);
+ self
+ }
+
+ fn items_end(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().align_items = Some(AlignItems::FlexEnd);
+ self
+ }
+
+ fn items_center(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().align_items = Some(AlignItems::Center);
+ self
+ }
+
+ fn justify_between(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
+ self
+ }
+
+ fn justify_center(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().justify_content = Some(JustifyContent::Center);
+ self
+ }
+
+ fn justify_start(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().justify_content = Some(JustifyContent::Start);
+ self
+ }
+
+ fn justify_end(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().justify_content = Some(JustifyContent::End);
+ self
+ }
+
+ fn justify_around(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
+ self
+ }
+
+ fn fill<F>(mut self, fill: F) -> Self
+ where
+ F: Into<Fill>,
+ Self: Sized,
+ {
+ self.declared_style().fill = Some(fill.into());
+ self
+ }
+
+ fn border_color<C>(mut self, border_color: C) -> Self
+ where
+ C: Into<Hsla>,
+ Self: Sized,
+ {
+ self.declared_style().border_color = Some(border_color.into());
+ self
+ }
+
+ fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
+ let style: &mut StyleRefinement = self.declared_style();
+ &mut style.text
+ }
+
+ fn text_color(mut self, color: impl Into<Hsla>) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
+ self
+ }
+
+ fn text_xs(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(0.75));
+ self
+ }
+
+ fn text_sm(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(0.875));
+ self
+ }
+
+ fn text_base(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(1.0));
+ self
+ }
+
+ fn text_lg(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(1.125));
+ self
+ }
+
+ fn text_xl(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(1.25));
+ self
+ }
+
+ fn text_2xl(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(1.5));
+ self
+ }
+
+ fn text_3xl(mut self) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_size = Some(rems(1.875));
+ self
+ }
+
+ fn font(mut self, family_name: impl Into<SharedString>) -> Self
+ where
+ Self: Sized,
+ {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .font_family = Some(family_name.into());
+ self
+ }
+}
@@ -0,0 +1,26 @@
+use crate::{Refineable, RefinementCascade};
+
+pub trait Styled {
+ type Style: Refineable + Default;
+
+ fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style>;
+ fn declared_style(&mut self) -> &mut <Self::Style as Refineable>::Refinement;
+
+ fn computed_style(&mut self) -> Self::Style {
+ Self::Style::from_refinement(&self.style_cascade().merged())
+ }
+
+ // fn hover(self) -> Hoverable<Self>
+ // where
+ // Self: Sized,
+ // {
+ // hoverable(self)
+ // }
+
+ // fn active(self) -> Pressable<Self>
+ // where
+ // Self: Sized,
+ // {
+ // pressable(self)
+ // }
+}
@@ -0,0 +1,383 @@
+use super::{
+ AbsoluteLength, Bounds, DefiniteLength, Edges, Layout, Length, Pixels, Point, Result, Size,
+ Style,
+};
+use collections::HashMap;
+use std::fmt::Debug;
+use taffy::{
+ geometry::Size as TaffySize,
+ style::AvailableSpace as TaffyAvailableSpace,
+ tree::{Measurable, MeasureFunc, NodeId},
+ Taffy,
+};
+
+pub struct TaffyLayoutEngine {
+ taffy: Taffy,
+ children_to_parents: HashMap<LayoutId, LayoutId>,
+ absolute_layouts: HashMap<LayoutId, Layout>,
+}
+
+impl TaffyLayoutEngine {
+ pub fn new() -> Self {
+ TaffyLayoutEngine {
+ taffy: Taffy::new(),
+ children_to_parents: HashMap::default(),
+ absolute_layouts: HashMap::default(),
+ }
+ }
+
+ pub fn request_layout(
+ &mut self,
+ style: Style,
+ rem_size: Pixels,
+ children: &[LayoutId],
+ ) -> Result<LayoutId> {
+ let style = style.to_taffy(rem_size);
+ if children.is_empty() {
+ Ok(self.taffy.new_leaf(style)?.into())
+ } else {
+ let parent_id = self
+ .taffy
+ // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
+ .new_with_children(style, unsafe { std::mem::transmute(children) })?
+ .into();
+ for child_id in children {
+ self.children_to_parents.insert(*child_id, parent_id);
+ }
+ Ok(parent_id)
+ }
+ }
+
+ pub fn request_measured_layout(
+ &mut self,
+ style: Style,
+ rem_size: Pixels,
+ measure: impl Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>
+ + Send
+ + Sync
+ + 'static,
+ ) -> Result<LayoutId> {
+ let style = style.to_taffy(rem_size);
+
+ let measurable = Box::new(Measureable(measure)) as Box<dyn Measurable>;
+ Ok(self
+ .taffy
+ .new_leaf_with_measure(style, MeasureFunc::Boxed(measurable))?
+ .into())
+ }
+
+ pub fn compute_layout(
+ &mut self,
+ id: LayoutId,
+ available_space: Size<AvailableSpace>,
+ ) -> Result<()> {
+ self.taffy
+ .compute_layout(id.into(), available_space.into())?;
+ Ok(())
+ }
+
+ pub fn layout(&mut self, id: LayoutId) -> Result<Layout> {
+ if let Some(layout) = self.absolute_layouts.get(&id).cloned() {
+ return Ok(layout);
+ }
+
+ let mut relative_layout: Layout = self.taffy.layout(id.into()).map(Into::into)?;
+ if let Some(parent_id) = self.children_to_parents.get(&id).copied() {
+ let parent_layout = self.layout(parent_id)?;
+ relative_layout.bounds.origin += parent_layout.bounds.origin;
+ }
+ self.absolute_layouts.insert(id, relative_layout.clone());
+
+ Ok(relative_layout)
+ }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[repr(transparent)]
+pub struct LayoutId(NodeId);
+
+impl std::hash::Hash for LayoutId {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ u64::from(self.0).hash(state);
+ }
+}
+
+impl From<NodeId> for LayoutId {
+ fn from(node_id: NodeId) -> Self {
+ Self(node_id)
+ }
+}
+
+impl From<LayoutId> for NodeId {
+ fn from(layout_id: LayoutId) -> NodeId {
+ layout_id.0
+ }
+}
+
+struct Measureable<F>(F);
+
+impl<F> taffy::tree::Measurable for Measureable<F>
+where
+ F: Send + Sync + Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>,
+{
+ fn measure(
+ &self,
+ known_dimensions: TaffySize<Option<f32>>,
+ available_space: TaffySize<TaffyAvailableSpace>,
+ ) -> TaffySize<f32> {
+ let known_dimensions: Size<Option<f32>> = known_dimensions.into();
+ let known_dimensions: Size<Option<Pixels>> = known_dimensions.map(|d| d.map(Into::into));
+ let available_space = available_space.into();
+ let size = (self.0)(known_dimensions, available_space);
+ size.into()
+ }
+}
+
+trait ToTaffy<Output> {
+ fn to_taffy(&self, rem_size: Pixels) -> Output;
+}
+
+impl ToTaffy<taffy::style::Style> for Style {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style {
+ taffy::style::Style {
+ display: self.display,
+ overflow: self.overflow.clone().into(),
+ scrollbar_width: self.scrollbar_width,
+ position: self.position,
+ inset: self.inset.to_taffy(rem_size),
+ size: self.size.to_taffy(rem_size),
+ min_size: self.min_size.to_taffy(rem_size),
+ max_size: self.max_size.to_taffy(rem_size),
+ aspect_ratio: self.aspect_ratio,
+ margin: self.margin.to_taffy(rem_size),
+ padding: self.padding.to_taffy(rem_size),
+ border: self.border_widths.to_taffy(rem_size),
+ align_items: self.align_items,
+ align_self: self.align_self,
+ align_content: self.align_content,
+ justify_content: self.justify_content,
+ gap: self.gap.to_taffy(rem_size),
+ flex_direction: self.flex_direction,
+ flex_wrap: self.flex_wrap,
+ flex_basis: self.flex_basis.to_taffy(rem_size),
+ flex_grow: self.flex_grow,
+ flex_shrink: self.flex_shrink,
+ ..Default::default() // Ignore grid properties for now
+ }
+ }
+}
+
+// impl ToTaffy for Bounds<Length> {
+// type Output = taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto>;
+
+// fn to_taffy(
+// &self,
+// rem_size: Pixels,
+// ) -> taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto> {
+// taffy::prelude::Bounds {
+// origin: self.origin.to_taffy(rem_size),
+// size: self.size.to_taffy(rem_size),
+// }
+// }
+// }
+
+impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
+ match self {
+ Length::Definite(length) => length.to_taffy(rem_size),
+ Length::Auto => taffy::prelude::LengthPercentageAuto::Auto,
+ }
+ }
+}
+
+impl ToTaffy<taffy::style::Dimension> for Length {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension {
+ match self {
+ Length::Definite(length) => length.to_taffy(rem_size),
+ Length::Auto => taffy::prelude::Dimension::Auto,
+ }
+ }
+}
+
+impl ToTaffy<taffy::style::LengthPercentage> for DefiniteLength {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
+ match self {
+ DefiniteLength::Absolute(length) => match length {
+ AbsoluteLength::Pixels(pixels) => {
+ taffy::style::LengthPercentage::Length(pixels.into())
+ }
+ AbsoluteLength::Rems(rems) => {
+ taffy::style::LengthPercentage::Length((*rems * rem_size).into())
+ }
+ },
+ DefiniteLength::Fraction(fraction) => {
+ taffy::style::LengthPercentage::Percent(*fraction)
+ }
+ }
+ }
+}
+
+impl ToTaffy<taffy::style::LengthPercentageAuto> for DefiniteLength {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentageAuto {
+ match self {
+ DefiniteLength::Absolute(length) => match length {
+ AbsoluteLength::Pixels(pixels) => {
+ taffy::style::LengthPercentageAuto::Length(pixels.into())
+ }
+ AbsoluteLength::Rems(rems) => {
+ taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into())
+ }
+ },
+ DefiniteLength::Fraction(fraction) => {
+ taffy::style::LengthPercentageAuto::Percent(*fraction)
+ }
+ }
+ }
+}
+
+impl ToTaffy<taffy::style::Dimension> for DefiniteLength {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension {
+ match self {
+ DefiniteLength::Absolute(length) => match length {
+ AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()),
+ AbsoluteLength::Rems(rems) => {
+ taffy::style::Dimension::Length((*rems * rem_size).into())
+ }
+ },
+ DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction),
+ }
+ }
+}
+
+impl ToTaffy<taffy::style::LengthPercentage> for AbsoluteLength {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage {
+ match self {
+ AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()),
+ AbsoluteLength::Rems(rems) => {
+ taffy::style::LengthPercentage::Length((*rems * rem_size).into())
+ }
+ }
+ }
+}
+
+impl<T, T2: Clone + Debug> From<taffy::geometry::Point<T>> for Point<T2>
+where
+ T: Into<T2>,
+{
+ fn from(point: taffy::geometry::Point<T>) -> Point<T2> {
+ Point {
+ x: point.x.into(),
+ y: point.y.into(),
+ }
+ }
+}
+
+impl<T: Clone + Debug, T2> Into<taffy::geometry::Point<T2>> for Point<T>
+where
+ T: Into<T2>,
+{
+ fn into(self) -> taffy::geometry::Point<T2> {
+ taffy::geometry::Point {
+ x: self.x.into(),
+ y: self.y.into(),
+ }
+ }
+}
+
+impl<T: ToTaffy<U> + Clone + Debug, U> ToTaffy<taffy::geometry::Size<U>> for Size<T> {
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::geometry::Size<U> {
+ taffy::geometry::Size {
+ width: self.width.to_taffy(rem_size).into(),
+ height: self.height.to_taffy(rem_size).into(),
+ }
+ }
+}
+
+impl<T, U> ToTaffy<taffy::geometry::Rect<U>> for Edges<T>
+where
+ T: ToTaffy<U> + Clone + Debug,
+{
+ fn to_taffy(&self, rem_size: Pixels) -> taffy::geometry::Rect<U> {
+ taffy::geometry::Rect {
+ top: self.top.to_taffy(rem_size).into(),
+ right: self.right.to_taffy(rem_size).into(),
+ bottom: self.bottom.to_taffy(rem_size).into(),
+ left: self.left.to_taffy(rem_size).into(),
+ }
+ }
+}
+
+impl<T: Into<U>, U: Clone + Debug> From<TaffySize<T>> for Size<U> {
+ fn from(taffy_size: taffy::geometry::Size<T>) -> Self {
+ Size {
+ width: taffy_size.width.into(),
+ height: taffy_size.height.into(),
+ }
+ }
+}
+
+impl<T: Into<U> + Clone + Debug, U> From<Size<T>> for taffy::geometry::Size<U> {
+ fn from(size: Size<T>) -> Self {
+ taffy::geometry::Size {
+ width: size.width.into(),
+ height: size.height.into(),
+ }
+ }
+}
+
+// impl From<TaffySize<Option<f32>>> for Size<Option<Pixels>> {
+// fn from(value: TaffySize<Option<f32>>) -> Self {
+// Self {
+// width: value.width.map(Into::into),
+// height: value.height.map(Into::into),
+// }
+// }
+// }
+
+#[derive(Copy, Clone, Debug)]
+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
+ MinContent,
+ /// The amount of space available is indefinite and the node should be laid out under a max-content constraint
+ MaxContent,
+}
+
+impl From<AvailableSpace> 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<TaffyAvailableSpace> 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<Pixels> for AvailableSpace {
+ fn from(pixels: Pixels) -> Self {
+ AvailableSpace::Definite(pixels)
+ }
+}
+
+impl From<&taffy::tree::Layout> for Layout {
+ fn from(layout: &taffy::tree::Layout) -> Self {
+ Layout {
+ order: layout.order,
+ bounds: Bounds {
+ origin: layout.location.into(),
+ size: layout.size.into(),
+ },
+ }
+ }
+}
@@ -0,0 +1,477 @@
+mod font_features;
+mod line_wrapper;
+mod text_layout_cache;
+
+use anyhow::anyhow;
+pub use font_features::*;
+use line_wrapper::*;
+pub use text_layout_cache::*;
+
+use crate::{
+ px, Bounds, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, UnderlineStyle,
+};
+use collections::HashMap;
+use core::fmt;
+use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use std::{
+ fmt::{Debug, Display, Formatter},
+ hash::{Hash, Hasher},
+ ops::{Deref, DerefMut},
+ sync::Arc,
+};
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
+pub struct FontId(pub usize);
+
+#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
+pub struct FontFamilyId(pub usize);
+
+pub struct TextSystem {
+ text_layout_cache: Arc<TextLayoutCache>,
+ platform_text_system: Arc<dyn PlatformTextSystem>,
+ font_ids_by_font: RwLock<HashMap<Font, FontId>>,
+ fonts_by_font_id: RwLock<HashMap<FontId, Font>>,
+ font_metrics: RwLock<HashMap<Font, FontMetrics>>,
+ wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
+ font_runs_pool: Mutex<Vec<Vec<(usize, FontId)>>>,
+}
+
+impl TextSystem {
+ pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
+ TextSystem {
+ text_layout_cache: Arc::new(TextLayoutCache::new(platform_text_system.clone())),
+ platform_text_system,
+ font_metrics: RwLock::new(HashMap::default()),
+ font_ids_by_font: RwLock::new(HashMap::default()),
+ fonts_by_font_id: RwLock::new(HashMap::default()),
+ wrapper_pool: Mutex::new(HashMap::default()),
+ font_runs_pool: Default::default(),
+ }
+ }
+
+ pub fn font_id(&self, font: &Font) -> Result<FontId> {
+ let font_id = self.font_ids_by_font.read().get(font).copied();
+
+ if let Some(font_id) = font_id {
+ Ok(font_id)
+ } else {
+ let font_id = self.platform_text_system.font_id(font)?;
+ self.font_ids_by_font.write().insert(font.clone(), font_id);
+ self.fonts_by_font_id.write().insert(font_id, font.clone());
+ Ok(font_id)
+ }
+ }
+
+ pub fn with_font<T>(&self, font_id: FontId, f: impl FnOnce(&Self, &Font) -> T) -> Result<T> {
+ self.fonts_by_font_id
+ .read()
+ .get(&font_id)
+ .ok_or_else(|| anyhow!("font not found"))
+ .map(|font| f(self, font))
+ }
+
+ pub fn bounding_box(&self, font: &Font, font_size: Pixels) -> Result<Bounds<Pixels>> {
+ self.read_metrics(&font, |metrics| metrics.bounding_box(font_size))
+ }
+
+ pub fn typographic_bounds(
+ &self,
+ font: &Font,
+ font_size: Pixels,
+ character: char,
+ ) -> Result<Bounds<Pixels>> {
+ let font_id = self.font_id(font)?;
+ let glyph_id = self
+ .platform_text_system
+ .glyph_for_char(font_id, character)
+ .ok_or_else(|| anyhow!("glyph not found for character '{}'", character))?;
+ let bounds = self
+ .platform_text_system
+ .typographic_bounds(font_id, glyph_id)?;
+ self.read_metrics(font, |metrics| {
+ (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
+ })
+ }
+
+ pub fn advance(&self, font: &Font, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
+ let font_id = self.font_id(font)?;
+ let glyph_id = self
+ .platform_text_system
+ .glyph_for_char(font_id, ch)
+ .ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
+ let result =
+ self.platform_text_system.advance(font_id, glyph_id)? / self.units_per_em(font)? as f32;
+
+ Ok(result * font_size)
+ }
+
+ pub fn units_per_em(&self, font: &Font) -> Result<u32> {
+ self.read_metrics(font, |metrics| metrics.units_per_em as u32)
+ }
+
+ pub fn cap_height(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font, |metrics| metrics.cap_height(font_size))
+ }
+
+ pub fn x_height(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font, |metrics| metrics.x_height(font_size))
+ }
+
+ pub fn ascent(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font, |metrics| metrics.ascent(font_size))
+ }
+
+ pub fn descent(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font, |metrics| metrics.descent(font_size))
+ }
+
+ pub fn baseline_offset(
+ &self,
+ font: &Font,
+ font_size: Pixels,
+ line_height: Pixels,
+ ) -> Result<Pixels> {
+ let ascent = self.ascent(font, font_size)?;
+ let descent = self.descent(font, font_size)?;
+ let padding_top = (line_height - ascent - descent) / 2.;
+ Ok(padding_top + ascent)
+ }
+
+ fn read_metrics<T>(&self, font: &Font, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+ let lock = self.font_metrics.upgradable_read();
+
+ if let Some(metrics) = lock.get(font) {
+ Ok(read(metrics))
+ } else {
+ let font_id = self.platform_text_system.font_id(&font)?;
+ let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
+ let metrics = lock
+ .entry(font.clone())
+ .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
+ Ok(read(metrics))
+ }
+ }
+
+ pub fn layout_line(
+ &self,
+ text: &str,
+ font_size: Pixels,
+ runs: &[(usize, RunStyle)],
+ ) -> Result<Line> {
+ let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+
+ dbg!("got font runs from pool");
+ let mut last_font: Option<&Font> = None;
+ for (len, style) in runs {
+ dbg!(len);
+ if let Some(last_font) = last_font.as_ref() {
+ dbg!("a");
+ if **last_font == style.font {
+ dbg!("b");
+ font_runs.last_mut().unwrap().0 += len;
+ dbg!("c");
+ continue;
+ }
+ dbg!("d");
+ }
+ dbg!("e");
+ last_font = Some(&style.font);
+ dbg!("f");
+ font_runs.push((*len, self.font_id(&style.font)?));
+ dbg!("g");
+ }
+
+ dbg!("built font runs");
+
+ let layout = self
+ .text_layout_cache
+ .layout_line(text, font_size, &font_runs);
+
+ font_runs.clear();
+ self.font_runs_pool.lock().push(font_runs);
+
+ Ok(Line::new(layout.clone(), runs))
+ }
+
+ pub fn finish_frame(&self) {
+ self.text_layout_cache.finish_frame()
+ }
+
+ pub fn line_wrapper(
+ self: &Arc<Self>,
+ font: Font,
+ font_size: Pixels,
+ ) -> Result<LineWrapperHandle> {
+ let lock = &mut self.wrapper_pool.lock();
+ let font_id = self.font_id(&font)?;
+ let wrappers = lock
+ .entry(FontIdWithSize { font_id, font_size })
+ .or_default();
+ let wrapper = wrappers.pop().map(anyhow::Ok).unwrap_or_else(|| {
+ Ok(LineWrapper::new(
+ font_id,
+ font_size,
+ self.platform_text_system.clone(),
+ ))
+ })?;
+
+ Ok(LineWrapperHandle {
+ wrapper: Some(wrapper),
+ text_system: self.clone(),
+ })
+ }
+}
+
+#[derive(Hash, Eq, PartialEq)]
+struct FontIdWithSize {
+ font_id: FontId,
+ font_size: Pixels,
+}
+
+pub struct LineWrapperHandle {
+ wrapper: Option<LineWrapper>,
+ text_system: Arc<TextSystem>,
+}
+
+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.clone(),
+ 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)]
+pub struct FontWeight(pub f32);
+
+impl Default for FontWeight {
+ #[inline]
+ fn default() -> FontWeight {
+ FontWeight::NORMAL
+ }
+}
+
+impl Hash for FontWeight {
+ fn hash<H: Hasher>(&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);
+}
+
+/// Allows italic or oblique faces to be selected.
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
+pub enum FontStyle {
+ /// A face that is neither italic not obliqued.
+ Normal,
+ /// A form that is generally cursive in nature.
+ Italic,
+ /// A typically-sloped version of the regular face.
+ Oblique,
+}
+
+impl Default for FontStyle {
+ fn default() -> FontStyle {
+ FontStyle::Normal
+ }
+}
+
+impl Display for FontStyle {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ Debug::fmt(self, f)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RunStyle {
+ pub font: Font,
+ pub color: Hsla,
+ pub underline: Option<UnderlineStyle>,
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct GlyphId(u32);
+
+impl From<GlyphId> for u32 {
+ fn from(value: GlyphId) -> Self {
+ value.0
+ }
+}
+
+impl From<u16> for GlyphId {
+ fn from(num: u16) -> Self {
+ GlyphId(num as u32)
+ }
+}
+
+impl From<u32> for GlyphId {
+ fn from(num: u32) -> Self {
+ GlyphId(num)
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Glyph {
+ pub id: GlyphId,
+ pub position: Point<Pixels>,
+ pub index: usize,
+ pub is_emoji: bool,
+}
+
+#[derive(Default, Debug)]
+pub struct LineLayout {
+ pub font_size: Pixels,
+ pub width: Pixels,
+ pub ascent: Pixels,
+ pub descent: Pixels,
+ pub runs: Vec<Run>,
+ pub len: usize,
+}
+
+#[derive(Debug)]
+pub struct Run {
+ pub font_id: FontId,
+ pub glyphs: Vec<Glyph>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct Font {
+ pub family: SharedString,
+ pub features: FontFeatures,
+ pub weight: FontWeight,
+ pub style: FontStyle,
+}
+
+pub fn font(family: impl Into<SharedString>) -> Font {
+ Font {
+ family: family.into(),
+ features: FontFeatures::default(),
+ weight: FontWeight::default(),
+ style: FontStyle::default(),
+ }
+}
+
+impl Font {
+ pub fn bold(mut self) -> Self {
+ self.weight = FontWeight::BOLD;
+ 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(crate) units_per_em: u32,
+
+ /// The vertical distance from the baseline of the font to the top of the glyph covers.
+ pub(crate) ascent: f32,
+
+ /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
+ pub(crate) descent: f32,
+
+ /// The recommended additional space to add between lines of type.
+ pub(crate) line_gap: f32,
+
+ /// The suggested position of the underline.
+ pub(crate) underline_position: f32,
+
+ /// The suggested thickness of the underline.
+ pub(crate) underline_thickness: f32,
+
+ /// The height of a capital letter measured from the baseline of the font.
+ pub(crate) cap_height: f32,
+
+ /// The height of a lowercase x.
+ pub(crate) x_height: f32,
+
+ /// The outer limits of the area that the font covers.
+ pub(crate) bounding_box: Bounds<f32>,
+}
+
+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<Pixels> {
+ (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
+ }
+}
@@ -0,0 +1,162 @@
+use schemars::{
+ schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
+ JsonSchema,
+};
+
+macro_rules! create_definitions {
+ ($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => {
+ #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+ pub struct FontFeatures {
+ enabled: u64,
+ disabled: u64,
+ }
+
+ impl FontFeatures {
+ $(
+ pub fn $name(&self) -> Option<bool> {
+ if (self.enabled & (1 << $idx)) != 0 {
+ Some(true)
+ } else if (self.disabled & (1 << $idx)) != 0 {
+ Some(false)
+ } else {
+ None
+ }
+ }
+ )*
+ }
+
+ impl std::fmt::Debug for FontFeatures {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut debug = f.debug_struct("FontFeatures");
+ $(
+ if let Some(value) = self.$name() {
+ debug.field(stringify!($name), &value);
+ };
+ )*
+ debug.finish()
+ }
+ }
+
+ impl<'de> serde::Deserialize<'de> for FontFeatures {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<M>(self, mut access: M) -> Result<Self::Value, M::Error>
+ where
+ M: MapAccess<'de>,
+ {
+ let mut enabled: u64 = 0;
+ let mut disabled: u64 = 0;
+
+ while let Some((key, value)) = access.next_entry::<String, Option<bool>>()? {
+ let idx = match key.as_str() {
+ $(stringify!($name) => $idx,)*
+ _ => continue,
+ };
+ match value {
+ Some(true) => enabled |= 1 << idx,
+ Some(false) => disabled |= 1 << idx,
+ None => {}
+ };
+ }
+ Ok(FontFeatures { enabled, disabled })
+ }
+ }
+
+ let features = deserializer.deserialize_map(FontFeaturesVisitor)?;
+ Ok(features)
+ }
+ }
+
+ impl serde::Serialize for FontFeatures {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ use serde::ser::SerializeMap;
+
+ let mut map = serializer.serialize_map(None)?;
+
+ $(
+ let feature = stringify!($name);
+ if let Some(value) = self.$name() {
+ map.serialize_entry(feature, &value)?;
+ }
+ )*
+
+ map.end()
+ }
+ }
+
+ impl JsonSchema for FontFeatures {
+ fn schema_name() -> String {
+ "FontFeatures".into()
+ }
+
+ fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema {
+ let mut schema = SchemaObject::default();
+ let properties = &mut schema.object().properties;
+ let feature_schema = Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))),
+ ..Default::default()
+ });
+
+ $(
+ properties.insert(stringify!($name).to_owned(), feature_schema.clone());
+ )*
+
+ schema.into()
+ }
+ }
+ };
+}
+
+create_definitions!(
+ (calt, 0),
+ (case, 1),
+ (cpsp, 2),
+ (frac, 3),
+ (liga, 4),
+ (onum, 5),
+ (ordn, 6),
+ (pnum, 7),
+ (ss01, 8),
+ (ss02, 9),
+ (ss03, 10),
+ (ss04, 11),
+ (ss05, 12),
+ (ss06, 13),
+ (ss07, 14),
+ (ss08, 15),
+ (ss09, 16),
+ (ss10, 17),
+ (ss11, 18),
+ (ss12, 19),
+ (ss13, 20),
+ (ss14, 21),
+ (ss15, 22),
+ (ss16, 23),
+ (ss17, 24),
+ (ss18, 25),
+ (ss19, 26),
+ (ss20, 27),
+ (subs, 28),
+ (sups, 29),
+ (swsh, 30),
+ (titl, 31),
+ (tnum, 32),
+ (zero, 33)
+);
@@ -0,0 +1,329 @@
+use crate::{px, FontId, Line, Pixels, PlatformTextSystem, ShapedBoundary};
+use collections::HashMap;
+use std::{iter, sync::Arc};
+
+pub struct LineWrapper {
+ platform_text_system: Arc<dyn PlatformTextSystem>,
+ pub(crate) font_id: FontId,
+ pub(crate) font_size: Pixels,
+ cached_ascii_char_widths: [Option<Pixels>; 128],
+ cached_other_char_widths: HashMap<char, Pixels>,
+}
+
+impl LineWrapper {
+ pub const MAX_INDENT: u32 = 256;
+
+ pub fn new(
+ font_id: FontId,
+ font_size: Pixels,
+ text_system: Arc<dyn PlatformTextSystem>,
+ ) -> Self {
+ Self {
+ platform_text_system: text_system,
+ font_id,
+ font_size,
+ cached_ascii_char_widths: [None; 128],
+ cached_other_char_widths: HashMap::default(),
+ }
+ }
+
+ pub fn wrap_line<'a>(
+ &'a mut self,
+ line: &'a str,
+ wrap_width: Pixels,
+ ) -> impl Iterator<Item = Boundary> + 'a {
+ let mut width = px(0.);
+ let mut first_non_whitespace_ix = None;
+ let mut 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 char_indices = line.char_indices();
+ iter::from_fn(move || {
+ for (ix, c) in char_indices.by_ref() {
+ if c == '\n' {
+ continue;
+ }
+
+ if self.is_boundary(prev_c, 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);
+ }
+
+ let char_width = self.width_for_char(c);
+ width += char_width;
+ if width > wrap_width && ix > last_wrap_ix {
+ if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
+ {
+ indent = Some(
+ Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
+ );
+ }
+
+ if last_candidate_ix > 0 {
+ 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(indent) = indent {
+ width += self.width_for_char(' ') * indent as f32;
+ }
+
+ return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
+ }
+ prev_c = c;
+ }
+
+ None
+ })
+ }
+
+ pub fn wrap_shaped_line<'a>(
+ &'a mut self,
+ str: &'a str,
+ line: &'a Line,
+ wrap_width: Pixels,
+ ) -> impl Iterator<Item = ShapedBoundary> + 'a {
+ let mut first_non_whitespace_ix = None;
+ let mut last_candidate_ix = None;
+ let mut last_candidate_x = px(0.);
+ let mut last_wrap_ix = ShapedBoundary {
+ run_ix: 0,
+ glyph_ix: 0,
+ };
+ let mut last_wrap_x = px(0.);
+ let mut prev_c = '\0';
+ let mut glyphs = line
+ .runs()
+ .iter()
+ .enumerate()
+ .flat_map(move |(run_ix, run)| {
+ run.glyphs()
+ .iter()
+ .enumerate()
+ .map(move |(glyph_ix, glyph)| {
+ let character = str[glyph.index..].chars().next().unwrap();
+ (
+ ShapedBoundary { run_ix, glyph_ix },
+ character,
+ glyph.position.x,
+ )
+ })
+ })
+ .peekable();
+
+ iter::from_fn(move || {
+ while let Some((ix, c, x)) = glyphs.next() {
+ if c == '\n' {
+ continue;
+ }
+
+ if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() {
+ last_candidate_ix = Some(ix);
+ last_candidate_x = x;
+ }
+
+ if c != ' ' && first_non_whitespace_ix.is_none() {
+ first_non_whitespace_ix = Some(ix);
+ }
+
+ let next_x = glyphs.peek().map_or(line.width(), |(_, _, x)| *x);
+ let width = next_x - last_wrap_x;
+ if width > wrap_width && ix > last_wrap_ix {
+ if let Some(last_candidate_ix) = last_candidate_ix.take() {
+ last_wrap_ix = last_candidate_ix;
+ last_wrap_x = last_candidate_x;
+ } else {
+ last_wrap_ix = ix;
+ last_wrap_x = x;
+ }
+
+ return Some(last_wrap_ix);
+ }
+ prev_c = c;
+ }
+
+ None
+ })
+ }
+
+ fn is_boundary(&self, prev: char, next: char) -> bool {
+ (prev == ' ') && (next != ' ')
+ }
+
+ #[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.compute_width_for_char(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.compute_width_for_char(c);
+ self.cached_other_char_widths.insert(c, width);
+ width
+ }
+ }
+ }
+
+ fn compute_width_for_char(&self, c: char) -> Pixels {
+ self.platform_text_system
+ .layout_line(&c.to_string(), self.font_size, &[(1, self.font_id)])
+ .width
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct Boundary {
+ pub ix: usize,
+ 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, App, RunStyle};
+
+ #[test]
+ fn test_wrap_line() {
+ App::test().run(|cx| {
+ let text_system = cx.text_system().clone();
+ let mut wrapper = LineWrapper::new(
+ text_system.font_id(&font("Courier")).unwrap(),
+ px(16.),
+ text_system.platform_text_system.clone(),
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line("aa bbb cccc ddddd eeee", px(72.))
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(12, 0),
+ Boundary::new(18, 0)
+ ],
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(4, 0),
+ Boundary::new(11, 0),
+ Boundary::new(18, 0)
+ ],
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line(" aaaaaaa", px(72.))
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 5),
+ Boundary::new(9, 5),
+ Boundary::new(11, 5),
+ ]
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line(" ", px(72.))
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(14, 0),
+ Boundary::new(21, 0)
+ ]
+ );
+ assert_eq!(
+ wrapper
+ .wrap_line(" aaaaaaaaaaaaaa", px(72.))
+ .collect::<Vec<_>>(),
+ &[
+ Boundary::new(7, 0),
+ Boundary::new(14, 3),
+ Boundary::new(18, 3),
+ Boundary::new(22, 3),
+ ]
+ );
+ });
+ }
+
+ // todo! repeat this test
+ #[test]
+ fn test_wrap_shaped_line() {
+ App::test().run(|cx| {
+ let text_system = cx.text_system().clone();
+
+ let normal = RunStyle {
+ font: font("Helvetica"),
+ color: Default::default(),
+ underline: Default::default(),
+ };
+ let bold = RunStyle {
+ font: font("Helvetica").bold(),
+ color: Default::default(),
+ underline: Default::default(),
+ };
+
+ let text = "aa bbb cccc ddddd eeee";
+ let line = text_system
+ .layout_line(
+ text,
+ px(16.),
+ &[
+ (4, normal.clone()),
+ (5, bold.clone()),
+ (6, normal.clone()),
+ (1, bold.clone()),
+ (7, normal.clone()),
+ ],
+ )
+ .unwrap();
+
+ let mut wrapper = LineWrapper::new(
+ text_system.font_id(&normal.font).unwrap(),
+ px(16.),
+ text_system.platform_text_system.clone(),
+ );
+ assert_eq!(
+ wrapper
+ .wrap_shaped_line(text, &line, px(72.))
+ .collect::<Vec<_>>(),
+ &[
+ ShapedBoundary {
+ run_ix: 1,
+ glyph_ix: 3
+ },
+ ShapedBoundary {
+ run_ix: 2,
+ glyph_ix: 3
+ },
+ ShapedBoundary {
+ run_ix: 4,
+ glyph_ix: 2
+ }
+ ],
+ );
+ });
+ }
+}
@@ -0,0 +1,478 @@
+use crate::{
+ black, point, px, Bounds, FontId, Glyph, Hsla, LineLayout, Pixels, PlatformTextSystem, Point,
+ Run, RunStyle, UnderlineStyle, WindowContext,
+};
+use anyhow::Result;
+use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
+use std::{
+ borrow::Borrow,
+ collections::HashMap,
+ hash::{Hash, Hasher},
+ sync::Arc,
+};
+
+pub(crate) struct TextLayoutCache {
+ prev_frame: Mutex<HashMap<CacheKeyValue, Arc<LineLayout>>>,
+ curr_frame: RwLock<HashMap<CacheKeyValue, Arc<LineLayout>>>,
+ platform_text_system: Arc<dyn PlatformTextSystem>,
+}
+
+impl TextLayoutCache {
+ pub fn new(fonts: Arc<dyn PlatformTextSystem>) -> Self {
+ Self {
+ prev_frame: Mutex::new(HashMap::new()),
+ curr_frame: RwLock::new(HashMap::new()),
+ platform_text_system: fonts,
+ }
+ }
+
+ pub fn finish_frame(&self) {
+ let mut prev_frame = self.prev_frame.lock();
+ let mut curr_frame = self.curr_frame.write();
+ std::mem::swap(&mut *prev_frame, &mut *curr_frame);
+ curr_frame.clear();
+ }
+
+ pub fn layout_line<'a>(
+ &'a self,
+ text: &'a str,
+ font_size: Pixels,
+ runs: &[(usize, FontId)],
+ ) -> Arc<LineLayout> {
+ dbg!("layout line");
+ let key = &CacheKeyRef {
+ text,
+ font_size,
+ runs,
+ } as &dyn CacheKey;
+ let curr_frame = self.curr_frame.upgradable_read();
+ if let Some(layout) = curr_frame.get(key) {
+ return layout.clone();
+ }
+
+ let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
+ if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
+ curr_frame.insert(key, layout.clone());
+ layout
+ } else {
+ let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+ let key = CacheKeyValue {
+ text: text.into(),
+ font_size,
+ runs: SmallVec::from(runs),
+ };
+ curr_frame.insert(key, layout.clone());
+ layout
+ }
+ }
+}
+
+trait CacheKey {
+ fn key(&self) -> CacheKeyRef;
+}
+
+impl<'a> PartialEq for (dyn CacheKey + 'a) {
+ fn eq(&self, other: &dyn CacheKey) -> bool {
+ self.key() == other.key()
+ }
+}
+
+impl<'a> Eq for (dyn CacheKey + 'a) {}
+
+impl<'a> Hash for (dyn CacheKey + 'a) {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.key().hash(state)
+ }
+}
+
+#[derive(Eq)]
+struct CacheKeyValue {
+ text: String,
+ font_size: Pixels,
+ runs: SmallVec<[(usize, FontId); 1]>,
+}
+
+impl CacheKey for CacheKeyValue {
+ fn key(&self) -> CacheKeyRef {
+ CacheKeyRef {
+ text: self.text.as_str(),
+ font_size: self.font_size,
+ runs: self.runs.as_slice(),
+ }
+ }
+}
+
+impl PartialEq for CacheKeyValue {
+ fn eq(&self, other: &Self) -> bool {
+ self.key().eq(&other.key())
+ }
+}
+
+impl Hash for CacheKeyValue {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.key().hash(state);
+ }
+}
+
+impl<'a> Borrow<dyn CacheKey + 'a> for CacheKeyValue {
+ fn borrow(&self) -> &(dyn CacheKey + 'a) {
+ self as &dyn CacheKey
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+struct CacheKeyRef<'a> {
+ text: &'a str,
+ font_size: Pixels,
+ runs: &'a [(usize, FontId)],
+}
+
+impl<'a> CacheKey for CacheKeyRef<'a> {
+ fn key(&self) -> CacheKeyRef {
+ *self
+ }
+}
+
+impl<'a> Hash for CacheKeyRef<'a> {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.text.hash(state);
+ self.font_size.hash(state);
+ for (len, font_id) in self.runs {
+ len.hash(state);
+ font_id.hash(state);
+ }
+ }
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct Line {
+ layout: Arc<LineLayout>,
+ style_runs: SmallVec<[StyleRun; 32]>,
+}
+
+#[derive(Debug, Clone)]
+struct StyleRun {
+ len: u32,
+ color: Hsla,
+ underline: UnderlineStyle,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ShapedBoundary {
+ pub run_ix: usize,
+ pub glyph_ix: usize,
+}
+
+impl Line {
+ pub fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
+ let mut style_runs = SmallVec::new();
+ for (len, style) in runs {
+ style_runs.push(StyleRun {
+ len: *len as u32,
+ color: style.color,
+ underline: style.underline.clone().unwrap_or_default(),
+ });
+ }
+ Self { layout, style_runs }
+ }
+
+ pub fn runs(&self) -> &[Run] {
+ &self.layout.runs
+ }
+
+ pub fn width(&self) -> Pixels {
+ self.layout.width
+ }
+
+ pub fn font_size(&self) -> Pixels {
+ self.layout.font_size
+ }
+
+ pub fn x_for_index(&self, index: usize) -> Pixels {
+ for run in &self.layout.runs {
+ for glyph in &run.glyphs {
+ if glyph.index >= index {
+ return glyph.position.x;
+ }
+ }
+ }
+ self.layout.width
+ }
+
+ pub fn font_for_index(&self, index: usize) -> Option<FontId> {
+ for run in &self.layout.runs {
+ for glyph in &run.glyphs {
+ if glyph.index >= index {
+ return Some(run.font_id);
+ }
+ }
+ }
+
+ None
+ }
+
+ pub fn len(&self) -> usize {
+ self.layout.len
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.layout.len == 0
+ }
+
+ pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
+ if x >= self.layout.width {
+ None
+ } else {
+ for run in self.layout.runs.iter().rev() {
+ for glyph in run.glyphs.iter().rev() {
+ if glyph.position.x <= x {
+ return Some(glyph.index);
+ }
+ }
+ }
+ Some(0)
+ }
+ }
+
+ // todo!
+ pub fn paint(
+ &self,
+ origin: Point<Pixels>,
+ visible_bounds: Bounds<Pixels>,
+ line_height: Pixels,
+ cx: &mut WindowContext,
+ ) -> Result<()> {
+ let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
+ let baseline_offset = point(px(0.), padding_top + self.layout.ascent);
+
+ let mut style_runs = self.style_runs.iter();
+ let mut run_end = 0;
+ let mut color = black();
+ let mut underline = None;
+
+ for run in &self.layout.runs {
+ cx.text_system().with_font(run.font_id, |system, font| {
+ let max_glyph_width = system.bounding_box(font, self.layout.font_size)?.size.width;
+
+ for glyph in &run.glyphs {
+ let glyph_origin = origin + baseline_offset + glyph.position;
+ if glyph_origin.x > visible_bounds.upper_right().x {
+ break;
+ }
+
+ let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+ if glyph.index >= run_end {
+ if let Some(style_run) = style_runs.next() {
+ if let Some((_, underline_style)) = &mut underline {
+ if style_run.underline != *underline_style {
+ finished_underline = underline.take();
+ }
+ }
+ if style_run.underline.thickness > px(0.) {
+ underline.get_or_insert((
+ point(
+ glyph_origin.x,
+ origin.y
+ + baseline_offset.y
+ + (self.layout.descent * 0.618),
+ ),
+ UnderlineStyle {
+ color: style_run.underline.color,
+ thickness: style_run.underline.thickness,
+ squiggly: style_run.underline.squiggly,
+ },
+ ));
+ }
+
+ run_end += style_run.len as usize;
+ color = style_run.color;
+ } else {
+ run_end = self.layout.len;
+ finished_underline = underline.take();
+ }
+ }
+
+ if glyph_origin.x + max_glyph_width < visible_bounds.origin.x {
+ continue;
+ }
+
+ if let Some((_underline_origin, _underline_style)) = finished_underline {
+ // cx.scene().insert(Underline {
+ // origin: underline_origin,
+ // width: glyph_origin.x - underline_origin.x,
+ // thickness: underline_style.thickness.into(),
+ // color: underline_style.color.unwrap(),
+ // squiggly: underline_style.squiggly,
+ // });
+ }
+
+ // if glyph.is_emoji {
+ // cx.scene().push_image_glyph(scene::ImageGlyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_origin,
+ // });
+ // } else {
+ // cx.scene().push_glyph(scene::Glyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_origin,
+ // color,
+ // });
+ // }
+ }
+
+ anyhow::Ok(())
+ })??;
+ }
+
+ if let Some((_underline_start, _underline_style)) = underline.take() {
+ let _line_end_x = origin.x + self.layout.width;
+ // cx.scene().push_underline(Underline {
+ // origin: underline_start,
+ // width: line_end_x - underline_start.x,
+ // color: underline_style.color,
+ // thickness: underline_style.thickness.into(),
+ // squiggly: underline_style.squiggly,
+ // });
+ }
+
+ Ok(())
+ }
+
+ pub fn paint_wrapped(
+ &self,
+ origin: Point<Pixels>,
+ _visible_bounds: Bounds<Pixels>,
+ line_height: Pixels,
+ boundaries: &[ShapedBoundary],
+ cx: &mut WindowContext,
+ ) -> Result<()> {
+ let padding_top = (line_height - self.layout.ascent - self.layout.descent) / 2.;
+ let baseline_offset = point(px(0.), padding_top + self.layout.ascent);
+
+ let mut boundaries = boundaries.into_iter().peekable();
+ let mut color_runs = self.style_runs.iter();
+ let mut style_run_end = 0;
+ let mut _color = black(); // todo!
+ let mut underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+
+ let mut glyph_origin = origin;
+ let mut prev_position = px(0.);
+ for (run_ix, run) in self.layout.runs.iter().enumerate() {
+ for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+ glyph_origin.x += glyph.position.x - prev_position;
+
+ if boundaries
+ .peek()
+ .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
+ {
+ boundaries.next();
+ if let Some((_underline_origin, _underline_style)) = underline.take() {
+ // cx.scene().push_underline(Underline {
+ // origin: underline_origin,
+ // width: glyph_origin.x - underline_origin.x,
+ // thickness: underline_style.thickness.into(),
+ // color: underline_style.color.unwrap(),
+ // squiggly: underline_style.squiggly,
+ // });
+ }
+
+ glyph_origin = point(origin.x, glyph_origin.y + line_height);
+ }
+ prev_position = glyph.position.x;
+
+ let mut finished_underline = None;
+ if glyph.index >= style_run_end {
+ if let Some(style_run) = color_runs.next() {
+ style_run_end += style_run.len as usize;
+ _color = style_run.color;
+ if let Some((_, underline_style)) = &mut underline {
+ if style_run.underline != *underline_style {
+ finished_underline = underline.take();
+ }
+ }
+ if style_run.underline.thickness > px(0.) {
+ underline.get_or_insert((
+ glyph_origin
+ + point(
+ px(0.),
+ baseline_offset.y + (self.layout.descent * 0.618),
+ ),
+ UnderlineStyle {
+ color: Some(
+ style_run.underline.color.unwrap_or(style_run.color),
+ ),
+ thickness: style_run.underline.thickness,
+ squiggly: style_run.underline.squiggly,
+ },
+ ));
+ }
+ } else {
+ style_run_end = self.layout.len;
+ _color = black();
+ finished_underline = underline.take();
+ }
+ }
+
+ if let Some((_underline_origin, _underline_style)) = finished_underline {
+ // cx.scene().push_underline(Underline {
+ // origin: underline_origin,
+ // width: glyph_origin.x - underline_origin.x,
+ // thickness: underline_style.thickness.into(),
+ // color: underline_style.color.unwrap(),
+ // squiggly: underline_style.squiggly,
+ // });
+ }
+
+ cx.text_system().with_font(run.font_id, |system, font| {
+ let _glyph_bounds = Bounds {
+ origin: glyph_origin,
+ size: system.bounding_box(font, self.layout.font_size)?.size,
+ };
+ // if glyph_bounds.intersects(visible_bounds) {
+ // if glyph.is_emoji {
+ // cx.scene().push_image_glyph(scene::ImageGlyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_bounds.origin() + baseline_offset,
+ // });
+ // } else {
+ // cx.scene().push_glyph(scene::Glyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_bounds.origin() + baseline_offset,
+ // color,
+ // });
+ // }
+ // }
+ anyhow::Ok(())
+ })??;
+ }
+ }
+
+ if let Some((_underline_origin, _underline_style)) = underline.take() {
+ // let line_end_x = glyph_origin.x + self.layout.width - prev_position;
+ // cx.scene().push_underline(Underline {
+ // origin: underline_origin,
+ // width: line_end_x - underline_origin.x,
+ // thickness: underline_style.thickness.into(),
+ // color: underline_style.color,
+ // squiggly: underline_style.squiggly,
+ // });
+ }
+
+ Ok(())
+ }
+}
+
+impl Run {
+ pub fn glyphs(&self) -> &[Glyph] {
+ &self.glyphs
+ }
+}
@@ -0,0 +1,49 @@
+use smol::future::FutureExt;
+use std::{future::Future, time::Duration};
+pub use util::*;
+
+pub fn post_inc(value: &mut usize) -> usize {
+ let prev = *value;
+ *value += 1;
+ prev
+}
+
+pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+where
+ F: Future<Output = T>,
+{
+ let timer = async {
+ smol::Timer::after(timeout).await;
+ Err(())
+ };
+ let future = async move { Ok(f.await) };
+ timer.race(future).await
+}
+
+#[cfg(any(test, feature = "test"))]
+pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);
+
+#[cfg(any(test, feature = "test"))]
+impl<'a> std::fmt::Debug for CwdBacktrace<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ use backtrace::{BacktraceFmt, BytesOrWideString};
+
+ let cwd = std::env::current_dir().unwrap();
+ let cwd = cwd.parent().unwrap();
+ let mut print_path = |fmt: &mut std::fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
+ std::fmt::Display::fmt(&path, fmt)
+ };
+ let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
+ for frame in self.0.frames() {
+ let mut formatted_frame = fmt.frame();
+ if frame
+ .symbols()
+ .iter()
+ .any(|s| s.filename().map_or(false, |f| f.starts_with(&cwd)))
+ {
+ formatted_frame.backtrace_frame(frame)?;
+ }
+ }
+ fmt.finish()
+ }
+}
@@ -0,0 +1,143 @@
+use parking_lot::Mutex;
+
+use crate::{
+ AnyElement, Element, Handle, IntoAnyElement, Layout, LayoutId, Result, ViewContext,
+ WindowContext,
+};
+use std::{any::Any, marker::PhantomData, sync::Arc};
+
+pub struct View<S: Send + Sync, P> {
+ state: Handle<S>,
+ render: Arc<dyn Fn(&mut S, &mut ViewContext<S>) -> AnyElement<S> + Send + Sync + 'static>,
+ parent_state_type: PhantomData<P>,
+}
+
+impl<S: 'static + Send + Sync, P: 'static + Send> View<S, P> {
+ pub fn into_any(self) -> AnyView<P> {
+ AnyView {
+ view: Arc::new(Mutex::new(self)),
+ parent_state_type: PhantomData,
+ }
+ }
+}
+
+impl<S: Send + Sync, P> Clone for View<S, P> {
+ fn clone(&self) -> Self {
+ Self {
+ state: self.state.clone(),
+ render: self.render.clone(),
+ parent_state_type: PhantomData,
+ }
+ }
+}
+
+pub type RootView<S> = View<S, ()>;
+
+pub fn view<S, P, E>(
+ state: Handle<S>,
+ render: impl Fn(&mut S, &mut ViewContext<S>) -> E + Send + Sync + 'static,
+) -> View<S, P>
+where
+ S: 'static + Send + Sync,
+ P: 'static,
+ E: Element<State = S>,
+{
+ View {
+ state,
+ render: Arc::new(move |state, cx| render(state, cx).into_any()),
+ parent_state_type: PhantomData,
+ }
+}
+
+impl<S: Send + Sync + 'static, P: Send + 'static> Element for View<S, P> {
+ type State = P;
+ type FrameState = AnyElement<S>;
+
+ fn layout(
+ &mut self,
+ _: &mut Self::State,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<(LayoutId, Self::FrameState)> {
+ self.state.update(cx, |state, cx| {
+ let mut element = (self.render)(state, cx);
+ let layout_id = element.layout(state, cx)?;
+ Ok((layout_id, element))
+ })
+ }
+
+ fn paint(
+ &mut self,
+ _: Layout,
+ _: &mut Self::State,
+ element: &mut Self::FrameState,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<()> {
+ self.state
+ .update(cx, |state, cx| element.paint(state, None, cx))
+ }
+}
+
+trait ViewObject: Send + 'static {
+ fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box<dyn Any>)>;
+ fn paint(
+ &mut self,
+ layout: Layout,
+ element: &mut dyn Any,
+ cx: &mut WindowContext,
+ ) -> Result<()>;
+}
+
+impl<S: Send + Sync + 'static, P: Send + 'static> ViewObject for View<S, P> {
+ fn layout(&mut self, cx: &mut WindowContext) -> Result<(LayoutId, Box<dyn Any>)> {
+ self.state.update(cx, |state, cx| {
+ let mut element = (self.render)(state, cx);
+ let layout_id = element.layout(state, cx)?;
+ let element = Box::new(element) as Box<dyn Any>;
+ Ok((layout_id, element))
+ })
+ }
+
+ fn paint(&mut self, _: Layout, element: &mut dyn Any, cx: &mut WindowContext) -> Result<()> {
+ self.state.update(cx, |state, cx| {
+ let element = element.downcast_mut::<AnyElement<S>>().unwrap();
+ element.paint(state, None, cx)
+ })
+ }
+}
+
+pub struct AnyView<S> {
+ view: Arc<Mutex<dyn ViewObject>>,
+ parent_state_type: PhantomData<S>,
+}
+
+impl<S: 'static> Element for AnyView<S> {
+ type State = ();
+ type FrameState = Box<dyn Any>;
+
+ fn layout(
+ &mut self,
+ _: &mut Self::State,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<(LayoutId, Self::FrameState)> {
+ self.view.lock().layout(cx)
+ }
+
+ fn paint(
+ &mut self,
+ layout: Layout,
+ _: &mut (),
+ element: &mut Box<dyn Any>,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<()> {
+ self.view.lock().paint(layout, element.as_mut(), cx)
+ }
+}
+
+impl<S> Clone for AnyView<S> {
+ fn clone(&self) -> Self {
+ Self {
+ view: self.view.clone(),
+ parent_state_type: PhantomData,
+ }
+ }
+}
@@ -0,0 +1,403 @@
+use crate::{
+ px, AnyView, AppContext, AvailableSpace, Bounds, Context, Effect, Element, EntityId, Handle,
+ LayoutId, MainThread, MainThreadOnly, Pixels, PlatformWindow, Point, Reference, Scene, Size,
+ StackContext, Style, TaffyLayoutEngine, WeakHandle, WindowOptions,
+};
+use anyhow::Result;
+use futures::Future;
+use std::{any::TypeId, marker::PhantomData, mem, sync::Arc};
+use util::ResultExt;
+
+pub struct AnyWindow {}
+
+pub struct Window {
+ handle: AnyWindowHandle,
+ platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
+ rem_size: Pixels,
+ content_size: Size<Pixels>,
+ layout_engine: TaffyLayoutEngine,
+ pub(crate) root_view: Option<AnyView<()>>,
+ mouse_position: Point<Pixels>,
+ pub(crate) scene: Scene,
+ pub(crate) dirty: bool,
+}
+
+impl Window {
+ pub fn new(
+ handle: AnyWindowHandle,
+ options: WindowOptions,
+ cx: &mut MainThread<AppContext>,
+ ) -> Self {
+ let platform_window = cx.platform().open_window(handle, options);
+ let mouse_position = platform_window.mouse_position();
+ let content_size = platform_window.content_size();
+ let scale_factor = platform_window.scale_factor();
+ platform_window.on_resize(Box::new({
+ let handle = handle;
+ let cx = cx.to_async();
+ move |content_size, scale_factor| {
+ cx.update_window(handle, |cx| {
+ cx.window.scene = Scene::new(scale_factor);
+ cx.window.content_size = content_size;
+ cx.window.dirty = true;
+ })
+ .log_err();
+ }
+ }));
+
+ let platform_window =
+ MainThreadOnly::new(Arc::new(platform_window), cx.platform().dispatcher());
+
+ Window {
+ handle,
+ platform_window,
+ rem_size: px(16.),
+ content_size,
+ layout_engine: TaffyLayoutEngine::new(),
+ root_view: None,
+ mouse_position,
+ scene: Scene::new(scale_factor),
+ dirty: true,
+ }
+ }
+}
+
+pub struct WindowContext<'a, 'w> {
+ app: Reference<'a, AppContext>,
+ window: Reference<'w, Window>,
+}
+
+impl<'a, 'w> WindowContext<'a, 'w> {
+ pub(crate) fn mutable(app: &'a mut AppContext, window: &'w mut Window) -> Self {
+ Self {
+ app: Reference::Mutable(app),
+ window: Reference::Mutable(window),
+ }
+ }
+
+ pub fn notify(&mut self) {
+ self.window.dirty = true;
+ }
+
+ pub fn request_layout(
+ &mut self,
+ style: Style,
+ children: impl IntoIterator<Item = LayoutId>,
+ ) -> Result<LayoutId> {
+ self.app.layout_id_buffer.clear();
+ self.app.layout_id_buffer.extend(children.into_iter());
+ let rem_size = self.rem_size();
+
+ self.window
+ .layout_engine
+ .request_layout(style, rem_size, &self.app.layout_id_buffer)
+ }
+
+ pub fn request_measured_layout<
+ F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync + 'static,
+ >(
+ &mut self,
+ style: Style,
+ rem_size: Pixels,
+ measure: F,
+ ) -> Result<LayoutId> {
+ self.window
+ .layout_engine
+ .request_measured_layout(style, rem_size, measure)
+ }
+
+ pub fn layout(&mut self, layout_id: LayoutId) -> Result<Layout> {
+ Ok(self
+ .window
+ .layout_engine
+ .layout(layout_id)
+ .map(Into::into)?)
+ }
+
+ pub fn rem_size(&self) -> Pixels {
+ self.window.rem_size
+ }
+
+ pub fn mouse_position(&self) -> Point<Pixels> {
+ self.window.mouse_position
+ }
+
+ pub fn scene(&mut self) -> &mut Scene {
+ &mut self.window.scene
+ }
+
+ pub fn run_on_main<R>(
+ &self,
+ f: impl FnOnce(&mut MainThread<WindowContext>) -> R + Send + 'static,
+ ) -> impl Future<Output = Result<R>>
+ where
+ R: Send + 'static,
+ {
+ let id = self.window.handle.id;
+ self.app.run_on_main(move |cx| {
+ cx.update_window(id, |cx| {
+ f(unsafe {
+ mem::transmute::<&mut WindowContext, &mut MainThread<WindowContext>>(cx)
+ })
+ })
+ })
+ }
+
+ pub(crate) fn draw(&mut self) -> Result<()> {
+ let unit_entity = self.unit_entity.clone();
+ self.update_entity(&unit_entity, |_, cx| {
+ let mut root_view = cx.window.root_view.take().unwrap();
+ let (root_layout_id, mut frame_state) = root_view.layout(&mut (), cx)?;
+ let available_space = cx.window.content_size.map(Into::into);
+
+ dbg!("computing layout");
+ cx.window
+ .layout_engine
+ .compute_layout(root_layout_id, available_space)?;
+ dbg!("asking for layout");
+ let layout = cx.window.layout_engine.layout(root_layout_id)?;
+
+ dbg!("painting root view");
+
+ root_view.paint(layout, &mut (), &mut frame_state, cx)?;
+ cx.window.root_view = Some(root_view);
+ let scene = cx.window.scene.take();
+
+ let _ = cx.run_on_main(|cx| {
+ cx.window
+ .platform_window
+ .borrow_on_main_thread()
+ .draw(scene);
+ });
+
+ cx.window.dirty = false;
+ Ok(())
+ })
+ }
+}
+
+impl MainThread<WindowContext<'_, '_>> {
+ // todo!("implement other methods that use platform window")
+ fn platform_window(&self) -> &dyn PlatformWindow {
+ self.window.platform_window.borrow_on_main_thread().as_ref()
+ }
+}
+
+impl Context for WindowContext<'_, '_> {
+ type EntityContext<'a, 'w, T: 'static + Send + Sync> = ViewContext<'a, 'w, T>;
+ type Result<T> = T;
+
+ fn entity<T: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T>) -> T,
+ ) -> Handle<T> {
+ let slot = self.app.entities.reserve();
+ let entity = build_entity(&mut ViewContext::mutable(
+ &mut *self.app,
+ &mut self.window,
+ slot.id,
+ ));
+ self.entities.redeem(slot, entity)
+ }
+
+ fn update_entity<T: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<T>,
+ update: impl FnOnce(&mut T, &mut Self::EntityContext<'_, '_, T>) -> R,
+ ) -> R {
+ let mut entity = self.entities.lease(handle);
+ let result = update(
+ &mut *entity,
+ &mut ViewContext::mutable(&mut *self.app, &mut *self.window, handle.id),
+ );
+ self.entities.end_lease(entity);
+ result
+ }
+}
+
+impl<'a, 'w> std::ops::Deref for WindowContext<'a, 'w> {
+ type Target = AppContext;
+
+ fn deref(&self) -> &Self::Target {
+ &self.app
+ }
+}
+
+impl<'a, 'w> std::ops::DerefMut for WindowContext<'a, 'w> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.app
+ }
+}
+
+impl<S> StackContext for ViewContext<'_, '_, S> {
+ fn app(&mut self) -> &mut AppContext {
+ &mut *self.window_cx.app
+ }
+
+ fn with_text_style<F, R>(&mut self, style: crate::TextStyleRefinement, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.window_cx.app.push_text_style(style);
+ let result = f(self);
+ self.window_cx.app.pop_text_style();
+ result
+ }
+
+ fn with_state<T: Send + Sync + 'static, F, R>(&mut self, state: T, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.window_cx.app.push_state(state);
+ let result = f(self);
+ self.window_cx.app.pop_state::<T>();
+ result
+ }
+}
+
+pub struct ViewContext<'a, 'w, S> {
+ window_cx: WindowContext<'a, 'w>,
+ entity_type: PhantomData<S>,
+ entity_id: EntityId,
+}
+
+impl<'a, 'w, S: Send + Sync + 'static> ViewContext<'a, 'w, S> {
+ fn mutable(app: &'a mut AppContext, window: &'w mut Window, entity_id: EntityId) -> Self {
+ Self {
+ window_cx: WindowContext::mutable(app, window),
+ entity_id,
+ entity_type: PhantomData,
+ }
+ }
+
+ pub fn handle(&self) -> WeakHandle<S> {
+ self.entities.weak_handle(self.entity_id)
+ }
+
+ pub fn observe<E: Send + Sync + 'static>(
+ &mut self,
+ handle: &Handle<E>,
+ on_notify: impl Fn(&mut S, Handle<E>, &mut ViewContext<'_, '_, S>) + Send + Sync + 'static,
+ ) {
+ let this = self.handle();
+ let handle = handle.downgrade();
+ let window_handle = self.window.handle;
+ self.app
+ .observers
+ .entry(handle.id)
+ .or_default()
+ .push(Arc::new(move |cx| {
+ cx.update_window(window_handle.id, |cx| {
+ if let Some(handle) = handle.upgrade(cx) {
+ this.update(cx, |this, cx| on_notify(this, handle, cx))
+ .is_ok()
+ } else {
+ false
+ }
+ })
+ .unwrap_or(false)
+ }));
+ }
+
+ pub fn notify(&mut self) {
+ self.window_cx.notify();
+ self.window_cx
+ .app
+ .pending_effects
+ .push_back(Effect::Notify(self.entity_id));
+ }
+
+ pub(crate) fn erase_state<R>(&mut self, f: impl FnOnce(&mut ViewContext<()>) -> R) -> R {
+ let entity_id = self.unit_entity.id;
+ let mut cx = ViewContext::mutable(
+ &mut *self.window_cx.app,
+ &mut *self.window_cx.window,
+ entity_id,
+ );
+ f(&mut cx)
+ }
+}
+
+impl<'a, 'w, S> Context for ViewContext<'a, 'w, S> {
+ type EntityContext<'b, 'c, U: 'static + Send + Sync> = ViewContext<'b, 'c, U>;
+ type Result<U> = U;
+
+ fn entity<T2: Send + Sync + 'static>(
+ &mut self,
+ build_entity: impl FnOnce(&mut Self::EntityContext<'_, '_, T2>) -> T2,
+ ) -> Handle<T2> {
+ self.window_cx.entity(build_entity)
+ }
+
+ fn update_entity<U: Send + Sync + 'static, R>(
+ &mut self,
+ handle: &Handle<U>,
+ update: impl FnOnce(&mut U, &mut Self::EntityContext<'_, '_, U>) -> R,
+ ) -> R {
+ self.window_cx.update_entity(handle, update)
+ }
+}
+
+impl<'a, 'w, S: 'static> std::ops::Deref for ViewContext<'a, 'w, S> {
+ type Target = WindowContext<'a, 'w>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.window_cx
+ }
+}
+
+impl<'a, 'w, S: 'static> std::ops::DerefMut for ViewContext<'a, 'w, S> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.window_cx
+ }
+}
+
+// #[derive(Clone, Copy, Eq, PartialEq, Hash)]
+slotmap::new_key_type! { pub struct WindowId; }
+
+#[derive(PartialEq, Eq)]
+pub struct WindowHandle<S> {
+ id: WindowId,
+ state_type: PhantomData<S>,
+}
+
+impl<S> Copy for WindowHandle<S> {}
+
+impl<S> Clone for WindowHandle<S> {
+ fn clone(&self) -> Self {
+ WindowHandle {
+ id: self.id,
+ state_type: PhantomData,
+ }
+ }
+}
+
+impl<S> WindowHandle<S> {
+ pub fn new(id: WindowId) -> Self {
+ WindowHandle {
+ id,
+ state_type: PhantomData,
+ }
+ }
+}
+
+impl<S: 'static> Into<AnyWindowHandle> for WindowHandle<S> {
+ fn into(self) -> AnyWindowHandle {
+ AnyWindowHandle {
+ id: self.id,
+ state_type: TypeId::of::<S>(),
+ }
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub struct AnyWindowHandle {
+ pub(crate) id: WindowId,
+ state_type: TypeId,
+}
+
+#[derive(Clone)]
+pub struct Layout {
+ pub order: u32,
+ pub bounds: Bounds<Pixels>,
+}
@@ -0,0 +1,14 @@
+[package]
+name = "gpui3_macros"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/gpui3_macros.rs"
+proc-macro = true
+
+[dependencies]
+syn = "1.0.72"
+quote = "1.0.9"
+proc-macro2 = "1.0.66"
@@ -0,0 +1,50 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput, GenericParam};
+
+pub fn derive_element(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = ast.ident;
+
+ let mut state_type = quote! { () };
+
+ for param in &ast.generics.params {
+ if let GenericParam::Type(type_param) = param {
+ let type_ident = &type_param.ident;
+ state_type = quote! {#type_ident};
+ }
+ }
+
+ let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
+
+ let gen = quote! {
+ impl #impl_generics gpui3::Element for #type_name #ty_generics
+ #where_clause
+ {
+ type State = #state_type;
+ type FrameState = gpui3::AnyElement<#state_type>;
+
+ fn layout(
+ &mut self,
+ state: &mut #state_type,
+ cx: &mut gpui3::ViewContext<V>,
+ ) -> anyhow::Result<(gpui3::LayoutId, Self::FrameState)> {
+ let mut rendered_element = self.render(state, cx).into_element().into_any();
+ let layout_id = rendered_element.layout(state, cx)?;
+ Ok((layout_id, rendered_element))
+ }
+
+ fn paint(
+ &mut self,
+ layout: &gpui3::Layout,
+ state: &mut #state_type,
+ rendered_element: &mut Self::FrameState,
+ cx: &mut gpui3::ViewContext<V>,
+ ) {
+ rendered_element.paint(layout.origin, state, cx);
+ }
+ }
+ };
+
+ gen.into()
+}
@@ -0,0 +1,14 @@
+use proc_macro::TokenStream;
+
+mod derive_element;
+mod style_helpers;
+
+#[proc_macro]
+pub fn style_helpers(args: TokenStream) -> TokenStream {
+ style_helpers::style_helpers(args)
+}
+
+#[proc_macro_derive(Element, attributes(element_crate))]
+pub fn derive_element(input: TokenStream) -> TokenStream {
+ derive_element::derive_element(input)
+}
@@ -0,0 +1,326 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{
+ parse::{Parse, ParseStream, Result},
+ parse_macro_input,
+};
+
+struct StyleableMacroInput;
+
+impl Parse for StyleableMacroInput {
+ fn parse(_input: ParseStream) -> Result<Self> {
+ Ok(StyleableMacroInput)
+ }
+}
+
+pub fn style_helpers(input: TokenStream) -> TokenStream {
+ let _ = parse_macro_input!(input as StyleableMacroInput);
+ let methods = generate_methods();
+ let output = quote! {
+ #(#methods)*
+ };
+ output.into()
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+ let mut methods = Vec::new();
+
+ for (prefix, auto_allowed, fields) in box_prefixes() {
+ for (suffix, length_tokens) in box_suffixes() {
+ if auto_allowed || suffix != "auto" {
+ let method = generate_method(prefix, suffix, &fields, length_tokens);
+ methods.push(method);
+ }
+ }
+ }
+
+ for (prefix, fields) in corner_prefixes() {
+ for (suffix, radius_tokens) in corner_suffixes() {
+ let method = generate_method(prefix, suffix, &fields, radius_tokens);
+ methods.push(method);
+ }
+ }
+
+ for (prefix, fields) in border_prefixes() {
+ for (suffix, width_tokens) in border_suffixes() {
+ let method = generate_method(prefix, suffix, &fields, width_tokens);
+ methods.push(method);
+ }
+ }
+
+ methods
+}
+
+fn generate_method(
+ prefix: &'static str,
+ suffix: &'static str,
+ fields: &Vec<TokenStream2>,
+ length_tokens: TokenStream2,
+) -> TokenStream2 {
+ let method_name = if suffix.is_empty() {
+ format_ident!("{}", prefix)
+ } else {
+ format_ident!("{}_{}", prefix, suffix)
+ };
+
+ let field_assignments = fields
+ .iter()
+ .map(|field_tokens| {
+ quote! {
+ style.#field_tokens = Some(gpui3::#length_tokens.into());
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let method = quote! {
+ fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+ let style = self.declared_style();
+ #(#field_assignments)*
+ self
+ }
+ };
+
+ method
+}
+
+fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
+ vec![
+ ("w", true, vec![quote! { size.width }]),
+ ("h", true, vec![quote! { size.height }]),
+ (
+ "size",
+ true,
+ vec![quote! {size.width}, quote! {size.height}],
+ ),
+ ("min_w", false, vec![quote! { min_size.width }]),
+ ("min_h", false, vec![quote! { min_size.height }]),
+ ("max_w", false, vec![quote! { max_size.width }]),
+ ("max_h", false, vec![quote! { max_size.height }]),
+ (
+ "m",
+ true,
+ vec![
+ quote! { margin.top },
+ quote! { margin.bottom },
+ quote! { margin.left },
+ quote! { margin.right },
+ ],
+ ),
+ ("mt", true, vec![quote! { margin.top }]),
+ ("mb", true, vec![quote! { margin.bottom }]),
+ (
+ "my",
+ true,
+ vec![quote! { margin.top }, quote! { margin.bottom }],
+ ),
+ (
+ "mx",
+ true,
+ vec![quote! { margin.left }, quote! { margin.right }],
+ ),
+ ("ml", true, vec![quote! { margin.left }]),
+ ("mr", true, vec![quote! { margin.right }]),
+ (
+ "p",
+ false,
+ vec![
+ quote! { padding.top },
+ quote! { padding.bottom },
+ quote! { padding.left },
+ quote! { padding.right },
+ ],
+ ),
+ ("pt", false, vec![quote! { padding.top }]),
+ ("pb", false, vec![quote! { padding.bottom }]),
+ (
+ "px",
+ false,
+ vec![quote! { padding.left }, quote! { padding.right }],
+ ),
+ (
+ "py",
+ false,
+ vec![quote! { padding.top }, quote! { padding.bottom }],
+ ),
+ ("pl", false, vec![quote! { padding.left }]),
+ ("pr", false, vec![quote! { padding.right }]),
+ ("top", true, vec![quote! { inset.top }]),
+ ("bottom", true, vec![quote! { inset.bottom }]),
+ ("left", true, vec![quote! { inset.left }]),
+ ("right", true, vec![quote! { inset.right }]),
+ (
+ "gap",
+ false,
+ vec![quote! { gap.width }, quote! { gap.height }],
+ ),
+ ("gap_x", false, vec![quote! { gap.width }]),
+ ("gap_y", false, vec![quote! { gap.height }]),
+ ]
+}
+
+fn box_suffixes() -> Vec<(&'static str, TokenStream2)> {
+ vec![
+ ("0", quote! { px(0.) }),
+ ("0p5", quote! { rems(0.125) }),
+ ("1", quote! { rems(0.25) }),
+ ("1p5", quote! { rems(0.375) }),
+ ("2", quote! { rems(0.5) }),
+ ("2p5", quote! { rems(0.625) }),
+ ("3", quote! { rems(0.75) }),
+ ("3p5", quote! { rems(0.875) }),
+ ("4", quote! { rems(1.) }),
+ ("5", quote! { rems(1.25) }),
+ ("6", quote! { rems(1.5) }),
+ ("7", quote! { rems(1.75) }),
+ ("8", quote! { rems(2.0) }),
+ ("9", quote! { rems(2.25) }),
+ ("10", quote! { rems(2.5) }),
+ ("11", quote! { rems(2.75) }),
+ ("12", quote! { rems(3.) }),
+ ("16", quote! { rems(4.) }),
+ ("20", quote! { rems(5.) }),
+ ("24", quote! { rems(6.) }),
+ ("32", quote! { rems(8.) }),
+ ("40", quote! { rems(10.) }),
+ ("48", quote! { rems(12.) }),
+ ("56", quote! { rems(14.) }),
+ ("64", quote! { rems(16.) }),
+ ("72", quote! { rems(18.) }),
+ ("80", quote! { rems(20.) }),
+ ("96", quote! { rems(24.) }),
+ ("auto", quote! { auto() }),
+ ("px", quote! { px(1.) }),
+ ("full", quote! { relative(1.) }),
+ ("1_2", quote! { relative(0.5) }),
+ ("1_3", quote! { relative(1./3.) }),
+ ("2_3", quote! { relative(2./3.) }),
+ ("1_4", quote! { relative(0.25) }),
+ ("2_4", quote! { relative(0.5) }),
+ ("3_4", quote! { relative(0.75) }),
+ ("1_5", quote! { relative(0.2) }),
+ ("2_5", quote! { relative(0.4) }),
+ ("3_5", quote! { relative(0.6) }),
+ ("4_5", quote! { relative(0.8) }),
+ ("1_6", quote! { relative(1./6.) }),
+ ("5_6", quote! { relative(5./6.) }),
+ ("1_12", quote! { relative(1./12.) }),
+ // ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
+ // ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
+ // ("screen", quote! { DefiniteLength::Vh(100.0) }),
+ ]
+}
+
+fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
+ vec![
+ (
+ "rounded",
+ vec![
+ quote! { corner_radii.top_left },
+ quote! { corner_radii.top_right },
+ quote! { corner_radii.bottom_right },
+ quote! { corner_radii.bottom_left },
+ ],
+ ),
+ (
+ "rounded_t",
+ vec![
+ quote! { corner_radii.top_left },
+ quote! { corner_radii.top_right },
+ ],
+ ),
+ (
+ "rounded_b",
+ vec![
+ quote! { corner_radii.bottom_left },
+ quote! { corner_radii.bottom_right },
+ ],
+ ),
+ (
+ "rounded_r",
+ vec![
+ quote! { corner_radii.top_right },
+ quote! { corner_radii.bottom_right },
+ ],
+ ),
+ (
+ "rounded_l",
+ vec![
+ quote! { corner_radii.top_left },
+ quote! { corner_radii.bottom_left },
+ ],
+ ),
+ ("rounded_tl", vec![quote! { corner_radii.top_left }]),
+ ("rounded_tr", vec![quote! { corner_radii.top_right }]),
+ ("rounded_bl", vec![quote! { corner_radii.bottom_left }]),
+ ("rounded_br", vec![quote! { corner_radii.bottom_right }]),
+ ]
+}
+
+fn corner_suffixes() -> Vec<(&'static str, TokenStream2)> {
+ vec![
+ ("none", quote! { px(0.) }),
+ ("sm", quote! { rems(0.125) }),
+ ("md", quote! { rems(0.25) }),
+ ("lg", quote! { rems(0.5) }),
+ ("xl", quote! { rems(0.75) }),
+ ("2xl", quote! { rems(1.) }),
+ ("3xl", quote! { rems(1.5) }),
+ ("full", quote! { px(9999.) }),
+ ]
+}
+
+fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
+ vec![
+ (
+ "border",
+ vec![
+ quote! { border_widths.top },
+ quote! { border_widths.right },
+ quote! { border_widths.bottom },
+ quote! { border_widths.left },
+ ],
+ ),
+ ("border_t", vec![quote! { border_widths.top }]),
+ ("border_b", vec![quote! { border_widths.bottom }]),
+ ("border_r", vec![quote! { border_widths.right }]),
+ ("border_l", vec![quote! { border_widths.left }]),
+ (
+ "border_x",
+ vec![
+ quote! { border_widths.left },
+ quote! { border_widths.right },
+ ],
+ ),
+ (
+ "border_y",
+ vec![
+ quote! { border_widths.top },
+ quote! { border_widths.bottom },
+ ],
+ ),
+ ]
+}
+
+fn border_suffixes() -> Vec<(&'static str, TokenStream2)> {
+ vec![
+ ("", quote! { px(1.) }),
+ ("0", quote! { px(0.) }),
+ ("1", quote! { px(1.) }),
+ ("2", quote! { px(2.) }),
+ ("3", quote! { px(3.) }),
+ ("4", quote! { px(4.) }),
+ ("5", quote! { px(5.) }),
+ ("6", quote! { px(6.) }),
+ ("7", quote! { px(7.) }),
+ ("8", quote! { px(8.) }),
+ ("9", quote! { px(9.) }),
+ ("10", quote! { px(10.) }),
+ ("11", quote! { px(11.) }),
+ ("12", quote! { px(12.) }),
+ ("16", quote! { px(16.) }),
+ ("20", quote! { px(20.) }),
+ ("24", quote! { px(24.) }),
+ ("32", quote! { px(32.) }),
+ ]
+}
@@ -35,7 +35,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
- // Create trait bound that each wrapped type must implement Clone & Default
+ // Create trait bound that each wrapped type must implement Clone // & Default
let type_param_bounds: Vec<_> = wrapped_types
.iter()
.map(|ty| {
@@ -51,13 +51,14 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
lifetimes: None,
path: parse_quote!(Clone),
}));
- punctuated.push_punct(syn::token::Add::default());
- punctuated.push_value(TypeParamBound::Trait(TraitBound {
- paren_token: None,
- modifier: syn::TraitBoundModifier::None,
- lifetimes: None,
- path: parse_quote!(Default),
- }));
+
+ // punctuated.push_punct(syn::token::Add::default());
+ // punctuated.push_value(TypeParamBound::Trait(TraitBound {
+ // paren_token: None,
+ // modifier: syn::TraitBoundModifier::None,
+ // lifetimes: None,
+ // path: parse_quote!(Default),
+ // }));
punctuated
},
})
@@ -161,7 +162,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
};
let gen = quote! {
- #[derive(Default, Clone)]
+ #[derive(Clone)]
pub struct #refinement_ident #impl_generics {
#( #field_visibilities #field_names: #wrapped_types ),*
}
@@ -186,6 +187,16 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
}
}
+ impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics
+ #where_clause
+ {
+ fn default() -> Self {
+ #refinement_ident {
+ #( #field_names: Default::default() ),*
+ }
+ }
+ }
+
impl #impl_generics #refinement_ident #ty_generics
#where_clause
{
@@ -17,6 +17,12 @@ pub trait Refineable: Clone {
{
Self::default().refined(refinement)
}
+ fn from_cascade(cascade: &RefinementCascade<Self>) -> Self
+ where
+ Self: Default + Sized,
+ {
+ Self::default().refined(&cascade.merged())
+ }
}
pub struct RefinementCascade<S: Refineable>(Vec<Option<S::Refinement>>);
@@ -8,15 +8,20 @@ publish = false
name = "storybook"
path = "src/storybook.rs"
+[features]
+test-support = []
+
[dependencies]
anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] }
chrono = "0.4"
+derive_more.workspace = true
fs = { path = "../fs" }
futures.workspace = true
gpui2 = { path = "../gpui2" }
itertools = "0.11.0"
log.workspace = true
+refineable = { path = "../refineable" }
rust-embed.workspace = true
serde.workspace = true
settings = { path = "../settings" }
@@ -0,0 +1,5 @@
+fn main() {
+ // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
+ // TODO: We shouldn't depend on WebRTC in editor
+ println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+}
@@ -0,0 +1,2919 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "socket2",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-net"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f"
+dependencies = [
+ "async-io",
+ "autocfg",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "autocfg",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+ "signal-hook",
+ "windows-sys",
+]
+
+[[package]]
+name = "async-task"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
+
+[[package]]
+name = "atomic"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide 0.7.1",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bindgen"
+version = "0.65.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.25",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "atomic-waker",
+ "fastrand",
+ "futures-lite",
+ "log",
+]
+
+[[package]]
+name = "bstr"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading 0.7.4",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "collections"
+version = "0.1.0"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "concurrent-queue"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-cstr"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "uuid 0.5.1",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "core-text"
+version = "19.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
+dependencies = [
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.63+curl-8.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "data-url"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading 0.8.0",
+]
+
+[[package]]
+name = "dwrote"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "winapi",
+ "wio",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erased-serde"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "etagere"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.7.1",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
+
+[[package]]
+name = "float-ord"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "font-kit"
+version = "0.11.0"
+source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "dirs-next",
+ "dwrote",
+ "float-ord",
+ "freetype",
+ "lazy_static",
+ "libc",
+ "log",
+ "pathfinder_geometry",
+ "pathfinder_simd",
+ "walkdir",
+ "winapi",
+ "yeslogic-fontconfig-sys",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1"
+dependencies = [
+ "log",
+ "memmap2",
+ "ttf-parser 0.12.3",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "freetype"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6"
+dependencies = [
+ "freetype-sys",
+ "libc",
+]
+
+[[package]]
+name = "freetype-sys"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a"
+dependencies = [
+ "cmake",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gimli"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "globset"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "gpui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-task",
+ "bindgen",
+ "block",
+ "cc",
+ "cocoa",
+ "collections",
+ "core-foundation",
+ "core-graphics",
+ "core-text",
+ "ctor",
+ "etagere",
+ "font-kit",
+ "foreign-types",
+ "futures",
+ "gpui_macros",
+ "image",
+ "itertools",
+ "lazy_static",
+ "log",
+ "media",
+ "metal",
+ "num_cpus",
+ "objc",
+ "ordered-float",
+ "parking",
+ "parking_lot 0.11.2",
+ "pathfinder_color",
+ "pathfinder_geometry",
+ "postage",
+ "rand",
+ "resvg",
+ "schemars",
+ "seahash",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smallvec",
+ "smol",
+ "sqlez",
+ "sum_tree",
+ "time",
+ "tiny-skia",
+ "usvg",
+ "util",
+ "uuid 1.4.0",
+ "waker-fn",
+]
+
+[[package]]
+name = "gpui_macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "gif",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+ "png",
+ "scoped_threadpool",
+ "tiff",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "isahc"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
+name = "kurbo"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449"
+dependencies = [
+ "arrayvec 0.7.4",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libloading"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
+dependencies = [
+ "cfg-if",
+ "windows-sys",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+dependencies = [
+ "serde",
+ "value-bag",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "media"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "block",
+ "bytes",
+ "core-foundation",
+ "foreign-types",
+ "metal",
+ "objc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memmap2"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metal"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4598d719460ade24c7d91f335daf055bf2a7eec030728ce751814c50cdd6a26c"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "foreign-types",
+ "log",
+ "objc",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[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.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[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 = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+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_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "object"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.8",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.3.5",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pathfinder_color"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69bdc0d277d559e35e1b374de56df9262a6b71e091ca04a8831a239f8c7f0c62"
+dependencies = [
+ "pathfinder_simd",
+]
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff"
+dependencies = [
+ "rustc_version",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pest"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pico-args"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
+
+[[package]]
+name = "pin-project"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "storybook"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
+[[package]]
+name = "png"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate",
+ "miniz_oxide 0.3.7",
+]
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
+[[package]]
+name = "pollster"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
+
+[[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 0.12.1",
+ "pin-project",
+ "pollster",
+ "static_assertions",
+ "thiserror",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[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",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rayon"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[package]]
+name = "rctree"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall 0.2.16",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
+
+[[package]]
+name = "resvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09697862c5c3f940cbaffef91969c62188b5c8ed385b0aef43a5ff01ddc8000f"
+dependencies = [
+ "jpeg-decoder",
+ "log",
+ "pico-args",
+ "png",
+ "rgb",
+ "svgfilters",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
+dependencies = [
+ "xmlparser",
+]
+
+[[package]]
+name = "rust-embed"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "6.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.25",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "7.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
+dependencies = [
+ "globset",
+ "sha2",
+ "walkdir",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustybuzz"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "smallvec",
+ "ttf-parser 0.9.0",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-general-category",
+ "unicode-script",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
+
+[[package]]
+name = "safe_arch"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05"
+dependencies = [
+ "bytemuck",
+]
+
+[[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 = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[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.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "signal-hook"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simplecss"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "smol"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-fs",
+ "async-io",
+ "async-lock",
+ "async-net",
+ "async-process",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "sqlez"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "futures",
+ "indoc",
+ "lazy_static",
+ "libsqlite3-sys",
+ "parking_lot 0.11.2",
+ "smol",
+ "thread_local",
+ "uuid 1.4.0",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "sum_tree"
+version = "0.1.0"
+dependencies = [
+ "arrayvec 0.7.4",
+ "log",
+]
+
+[[package]]
+name = "sval"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1"
+
+[[package]]
+name = "sval_buffer"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028"
+dependencies = [
+ "sval",
+ "sval_ref",
+]
+
+[[package]]
+name = "sval_dynamic"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_fmt"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_json"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d"
+dependencies = [
+ "itoa",
+ "ryu",
+ "sval",
+]
+
+[[package]]
+name = "sval_ref"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c"
+dependencies = [
+ "sval",
+]
+
+[[package]]
+name = "sval_serde"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046"
+dependencies = [
+ "serde",
+ "sval",
+ "sval_buffer",
+ "sval_fmt",
+]
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
+[[package]]
+name = "svgfilters"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e"
+dependencies = [
+ "float-cmp",
+ "rgb",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
+dependencies = [
+ "float-cmp",
+ "siphasher",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "take-until"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
+
+[[package]]
+name = "thiserror"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
+[[package]]
+name = "time"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+
+[[package]]
+name = "time-macros"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tiny-skia"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.5.2",
+ "bytemuck",
+ "cfg-if",
+ "png",
+ "safe_arch",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+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 = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.25",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca"
+
+[[package]]
+name = "ttf-parser"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
+
+[[package]]
+name = "unicode-general-category"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-script"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
+[[package]]
+name = "url"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "usvg"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8352f317d8f9a918ba5154797fb2a93e2730244041cf7d5be35148266adfa5"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb",
+ "kurbo",
+ "log",
+ "memmap2",
+ "pico-args",
+ "rctree",
+ "roxmltree",
+ "rustybuzz",
+ "simplecss",
+ "siphasher",
+ "svgtypes",
+ "ttf-parser 0.12.3",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
+[[package]]
+name = "util"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "backtrace",
+ "dirs",
+ "futures",
+ "isahc",
+ "lazy_static",
+ "log",
+ "rand",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "smol",
+ "take-until",
+ "url",
+]
+
+[[package]]
+name = "uuid"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
+
+[[package]]
+name = "uuid"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
+dependencies = [
+ "value-bag-serde1",
+ "value-bag-sval2",
+]
+
+[[package]]
+name = "value-bag-serde1"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_fmt",
+]
+
+[[package]]
+name = "value-bag-sval2"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
+dependencies = [
+ "sval",
+ "sval_buffer",
+ "sval_dynamic",
+ "sval_fmt",
+ "sval_json",
+ "sval_ref",
+ "sval_serde",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "weezl"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[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-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "wio"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
+
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
+[[package]]
+name = "yeslogic-fontconfig-sys"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386"
+dependencies = [
+ "const-cstr",
+ "dlib",
+ "once_cell",
+ "pkg-config",
+]
@@ -0,0 +1,25 @@
+[package]
+name = "storybook2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook2.rs"
+
+[dependencies]
+anyhow.workspace = true
+# TODO: Remove after diagnosing stack overflow.
+backtrace-on-stack-overflow = "0.3.0"
+gpui3 = { path = "../gpui3" }
+log.workspace = true
+rust-embed.workspace = true
+serde.workspace = true
+settings = { path = "../settings" }
+simplelog = "0.9"
+theme = { path = "../theme" }
+util = { path = "../util" }
+
+[dev-dependencies]
+gpui3 = { path = "../gpui3", features = ["test"] }
@@ -0,0 +1,5 @@
+fn main() {
+ // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
+ // TODO: We shouldn't depend on WebRTC in editor
+ println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+}
@@ -0,0 +1,72 @@
+Much of element styling is now handled by an external engine.
+
+
+How do I make an element hover.
+
+There's a hover style.
+
+Hoverable needs to wrap another element. That element can be styled.
+
+```rs
+struct Hoverable<E: Element> {
+
+}
+
+impl<V> Element<V> for Hoverable {
+
+}
+
+```
+
+
+
+```rs
+#[derive(Styled, Interactive)]
+pub struct Div {
+ declared_style: StyleRefinement,
+ interactions: Interactions
+}
+
+pub trait Styled {
+ fn declared_style(&mut self) -> &mut StyleRefinement;
+ fn compute_style(&mut self) -> Style {
+ Style::default().refine(self.declared_style())
+ }
+
+ // All the tailwind classes, modifying self.declared_style()
+}
+
+impl Style {
+ pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
+ pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
+}
+
+pub trait Interactive<V> {
+ fn interactions(&mut self) -> &mut Interactions<V>;
+
+ fn on_click(self, )
+}
+
+struct Interactions<V> {
+ click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
+}
+
+
+```
+
+
+```rs
+
+
+trait Stylable {
+ type Style;
+
+ fn with_style(self, style: Self::Style) -> Self;
+}
+
+
+
+
+
+
+```
@@ -0,0 +1,176 @@
+use crate::theme::{theme, Theme};
+use gpui3::{
+ div, img, svg, view, AppContext, ArcCow, Context, Element, IntoAnyElement, ParentElement,
+ ScrollState, StyleHelpers, View, ViewContext, WindowContext,
+};
+
+pub struct CollabPanel {
+ scroll_state: ScrollState,
+}
+
+pub fn collab_panel<S: 'static>(cx: &mut WindowContext) -> View<CollabPanel, S> {
+ view(cx.entity(|cx| CollabPanel::new(cx)), CollabPanel::render)
+}
+
+impl CollabPanel {
+ fn new(_: &mut AppContext) -> Self {
+ CollabPanel {
+ scroll_state: ScrollState::default(),
+ }
+ }
+}
+
+impl CollabPanel {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<State = Self> {
+ let theme = theme(cx);
+
+ // Panel
+ div()
+ .w_64()
+ .h_full()
+ .flex()
+ .flex_col()
+ .font("Zed Sans Extended")
+ .text_color(theme.middle.base.default.foreground)
+ .border_color(theme.middle.base.default.border)
+ .border()
+ .fill(theme.middle.base.default.background)
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .overflow_y_scroll(self.scroll_state.clone())
+ // List Container
+ .child(
+ div()
+ .fill(theme.lowest.base.default.background)
+ .pb_1()
+ .border_color(theme.lowest.base.default.border)
+ .border_b()
+ //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
+ // .group()
+ // List Section Header
+ .child(self.list_section_header("#CRDB", true, theme))
+ // List Item Large
+ .child(self.list_item(
+ "http://github.com/maxbrunsfeld.png?s=50",
+ "maxbrunsfeld",
+ theme,
+ )),
+ )
+ .child(
+ div()
+ .py_2()
+ .flex()
+ .flex_col()
+ .child(self.list_section_header("CHANNELS", true, theme)),
+ )
+ .child(
+ div()
+ .py_2()
+ .flex()
+ .flex_col()
+ .child(self.list_section_header("CONTACTS", true, theme))
+ .children(
+ std::iter::repeat_with(|| {
+ vec![
+ self.list_item(
+ "http://github.com/as-cii.png?s=50",
+ "as-cii",
+ theme,
+ ),
+ self.list_item(
+ "http://github.com/nathansobo.png?s=50",
+ "nathansobo",
+ theme,
+ ),
+ self.list_item(
+ "http://github.com/maxbrunsfeld.png?s=50",
+ "maxbrunsfeld",
+ theme,
+ ),
+ ]
+ })
+ .take(10)
+ .flatten(),
+ ),
+ ),
+ )
+ .child(
+ div()
+ .h_7()
+ .px_2()
+ .border_t()
+ .border_color(theme.middle.variant.default.border)
+ .flex()
+ .items_center()
+ .child(
+ div()
+ .text_sm()
+ .text_color(theme.middle.variant.default.foreground)
+ .child("Find..."),
+ ),
+ )
+ }
+
+ fn list_section_header(
+ &self,
+ label: impl IntoAnyElement<Self>,
+ expanded: bool,
+ theme: &Theme,
+ ) -> impl Element<State = Self> {
+ div()
+ .h_7()
+ .px_2()
+ .flex()
+ .justify_between()
+ .items_center()
+ .child(div().flex().gap_1().text_sm().child(label))
+ .child(
+ div().flex().h_full().gap_1().items_center().child(
+ svg()
+ .path(if expanded {
+ "icons/radix/caret-down.svg"
+ } else {
+ "icons/radix/caret-up.svg"
+ })
+ .w_3p5()
+ .h_3p5()
+ .fill(theme.middle.variant.default.foreground),
+ ),
+ )
+ }
+
+ fn list_item(
+ &self,
+ avatar_uri: impl Into<ArcCow<'static, str>>,
+ label: impl IntoAnyElement<Self>,
+ theme: &Theme,
+ ) -> impl Element<State = Self> {
+ div()
+ .h_7()
+ .px_2()
+ .flex()
+ .items_center()
+ // .hover()
+ // .fill(theme.lowest.variant.hovered.background)
+ // .active()
+ // .fill(theme.lowest.variant.pressed.background)
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_1()
+ .text_sm()
+ .child(
+ img()
+ .uri(avatar_uri)
+ .size_3p5()
+ .rounded_full()
+ .fill(theme.middle.positive.default.foreground),
+ )
+ .child(label),
+ )
+ }
+}
@@ -0,0 +1,97 @@
+use gpui2::{
+ elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
+ Element, EventContext, IntoElement, ParentElement, ViewContext,
+};
+use std::{marker::PhantomData, rc::Rc};
+
+struct ButtonHandlers<V, D> {
+ click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
+}
+
+impl<V, D> Default for ButtonHandlers<V, D> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+}
+
+#[derive(Element)]
+pub struct Button<V: 'static, D: 'static> {
+ handlers: ButtonHandlers<V, D>,
+ label: Option<ArcCow<'static, str>>,
+ icon: Option<ArcCow<'static, str>>,
+ data: Rc<D>,
+ view_type: PhantomData<V>,
+}
+
+// Impl block for buttons without data.
+// See below for an impl block for any button.
+impl<V: 'static> Button<V, ()> {
+ fn new() -> Self {
+ Self {
+ handlers: ButtonHandlers::default(),
+ label: None,
+ icon: None,
+ data: Rc::new(()),
+ view_type: PhantomData,
+ }
+ }
+
+ pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+ Button {
+ handlers: ButtonHandlers::default(),
+ label: self.label,
+ icon: self.icon,
+ data: Rc::new(data),
+ view_type: PhantomData,
+ }
+ }
+}
+
+// Impl block for button regardless of its data type.
+impl<V: 'static, D: 'static> Button<V, D> {
+ pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+ self.icon = Some(icon.into());
+ self
+ }
+
+ pub fn on_click(
+ mut self,
+ handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
+ ) -> Self {
+ self.handlers.click = Some(Rc::new(handler));
+ self
+ }
+}
+
+pub fn button<V>() -> Button<V, ()> {
+ Button::new()
+}
+
+impl<V: 'static, D: 'static> Button<V, D> {
+ fn render(
+ &mut self,
+ view: &mut V,
+ cx: &mut ViewContext<V>,
+ ) -> impl IntoElement<V> + Interactive<V> {
+ // let colors = &cx.theme::<Theme>().colors;
+
+ let button = div()
+ // .fill(colors.error(0.5))
+ .h_4()
+ .children(self.label.clone());
+
+ if let Some(handler) = self.handlers.click.clone() {
+ let data = self.data.clone();
+ button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+ handler(view, data.as_ref(), cx)
+ })
+ } else {
+ button
+ }
+ }
+}
@@ -0,0 +1,75 @@
+#![allow(dead_code, unused_variables)]
+
+use gpui3::{Bounds, WindowBounds, WindowOptions};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+
+mod collab_panel;
+mod theme;
+mod themes;
+mod workspace;
+
+// gpui2::actions! {
+// storybook,
+// [ToggleInspector]
+// }
+
+fn main() {
+ // unsafe { backtrace_on_stack_overflow::enable() };
+
+ SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+ gpui3::App::production().run(|cx| {
+ let window = cx.open_window(
+ WindowOptions {
+ bounds: WindowBounds::Fixed(Bounds {
+ size: gpui3::Size {
+ width: 800_f32.into(),
+ height: 600_f32.into(),
+ },
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ |cx| workspace(cx),
+ );
+
+ cx.activate(true);
+ });
+}
+
+use rust_embed::RustEmbed;
+use workspace::workspace;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "themes/**/*"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+// impl AssetSource for Assets {
+// fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
+// Self::get(path)
+// .map(|f| f.data)
+// .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+// }
+
+// fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
+// Self::iter().filter(|p| p.starts_with(path)).collect()
+// }
+// }
+
+// fn load_embedded_fonts(platform: &dyn gpui2::Platform) {
+// let font_paths = Assets.list("fonts");
+// let mut embedded_fonts = Vec::new();
+// for font_path in &font_paths {
+// if font_path.ends_with(".ttf") {
+// let font_path = &*font_path;
+// let font_bytes = Assets.load(font_path).unwrap().to_vec();
+// embedded_fonts.push(Arc::from(font_bytes));
+// }
+// }
+// platform.fonts().add_fonts(&embedded_fonts).unwrap();
+// }
@@ -0,0 +1,193 @@
+use gpui3::{Element, Hsla, Layout, LayoutId, Result, StackContext, ViewContext, WindowContext};
+use serde::{de::Visitor, Deserialize, Deserializer};
+use std::{collections::HashMap, fmt};
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Theme {
+ pub name: String,
+ pub is_light: bool,
+ pub lowest: Layer,
+ pub middle: Layer,
+ pub highest: Layer,
+ pub popover_shadow: Shadow,
+ pub modal_shadow: Shadow,
+ #[serde(deserialize_with = "deserialize_player_colors")]
+ pub players: Vec<PlayerColors>,
+ #[serde(deserialize_with = "deserialize_syntax_colors")]
+ pub syntax: HashMap<String, Hsla>,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Layer {
+ pub base: StyleSet,
+ pub variant: StyleSet,
+ pub on: StyleSet,
+ pub accent: StyleSet,
+ pub positive: StyleSet,
+ pub warning: StyleSet,
+ pub negative: StyleSet,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct StyleSet {
+ #[serde(rename = "default")]
+ pub default: ContainerColors,
+ pub hovered: ContainerColors,
+ pub pressed: ContainerColors,
+ pub active: ContainerColors,
+ pub disabled: ContainerColors,
+ pub inverted: ContainerColors,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct ContainerColors {
+ pub background: Hsla,
+ pub foreground: Hsla,
+ pub border: Hsla,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct PlayerColors {
+ pub selection: Hsla,
+ pub cursor: Hsla,
+}
+
+#[derive(Deserialize, Clone, Default, Debug)]
+pub struct Shadow {
+ pub blur: u8,
+ pub color: Hsla,
+ pub offset: Vec<u8>,
+}
+
+fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ struct PlayerArrayVisitor;
+
+ impl<'de> Visitor<'de> for PlayerArrayVisitor {
+ type Value = Vec<PlayerColors>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an object with integer keys")
+ }
+
+ fn visit_map<A: serde::de::MapAccess<'de>>(
+ self,
+ mut map: A,
+ ) -> Result<Self::Value, A::Error> {
+ let mut players = Vec::with_capacity(8);
+ while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
+ if key < 8 {
+ players.push(value);
+ } else {
+ return Err(serde::de::Error::invalid_value(
+ serde::de::Unexpected::Unsigned(key as u64),
+ &"a key in range 0..7",
+ ));
+ }
+ }
+ Ok(players)
+ }
+ }
+
+ deserializer.deserialize_map(PlayerArrayVisitor)
+}
+
+fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ #[derive(Deserialize)]
+ struct ColorWrapper {
+ color: Hsla,
+ }
+
+ struct SyntaxVisitor;
+
+ impl<'de> Visitor<'de> for SyntaxVisitor {
+ type Value = HashMap<String, Hsla>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a map with keys and objects with a single color field as values")
+ }
+
+ fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
+ where
+ M: serde::de::MapAccess<'de>,
+ {
+ let mut result = HashMap::new();
+ while let Some(key) = map.next_key()? {
+ let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
+ result.insert(key, wrapper.color);
+ }
+ Ok(result)
+ }
+ }
+ deserializer.deserialize_map(SyntaxVisitor)
+}
+
+pub fn themed<E, F>(theme: Theme, cx: &mut ViewContext<E::State>, build_child: F) -> Themed<E>
+where
+ E: Element,
+ F: FnOnce(&mut ViewContext<E::State>) -> E,
+{
+ let child = cx.with_state(theme.clone(), |cx| build_child(cx));
+ Themed { theme, child }
+}
+
+pub struct Themed<E> {
+ pub(crate) theme: Theme,
+ pub(crate) child: E,
+}
+
+impl<E: Element> Element for Themed<E> {
+ type State = E::State;
+ type FrameState = E::FrameState;
+
+ fn layout(
+ &mut self,
+ state: &mut E::State,
+ cx: &mut ViewContext<E::State>,
+ ) -> anyhow::Result<(LayoutId, Self::FrameState)>
+ where
+ Self: Sized,
+ {
+ cx.with_state(self.theme.clone(), |cx| self.child.layout(state, cx))
+ }
+
+ fn paint(
+ &mut self,
+ layout: Layout,
+ state: &mut Self::State,
+ frame_state: &mut Self::FrameState,
+ cx: &mut ViewContext<Self::State>,
+ ) -> Result<()>
+ where
+ Self: Sized,
+ {
+ cx.with_state(self.theme.clone(), |cx| {
+ self.child.paint(layout, state, frame_state, cx)
+ })
+ }
+}
+
+// fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
+// settings::get::<ThemeSettings>(cx)
+// .theme
+// .deserialized_base_theme
+// .lock()
+// .get_or_insert_with(|| {
+// let theme: Theme =
+// serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
+// .unwrap();
+// Box::new(theme)
+// })
+// .downcast_ref::<Theme>()
+// .unwrap()
+// .clone()
+// }
+
+pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
+ cx.state()
+}
@@ -0,0 +1,3 @@
+mod rose_pine_dawn;
+
+pub use rose_pine_dawn::*;
@@ -0,0 +1,845 @@
+use crate::theme::Theme;
+use gpui3::serde_json::{self, json};
+
+pub fn rose_pine_dawn() -> Theme {
+ serde_json::from_value(json! {
+ {
+ "name": "Rosé Pine",
+ "is_light": false,
+ "ramps": {},
+ "lowest": {
+ "base": {
+ "default": {
+ "background": "#292739",
+ "border": "#423f55",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#423f55",
+ "border": "#423f55",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#4e4b63",
+ "border": "#423f55",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#47445b",
+ "border": "#36334a",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#292739",
+ "border": "#353347",
+ "foreground": "#2f2b43"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#4b4860"
+ }
+ },
+ "variant": {
+ "default": {
+ "background": "#292739",
+ "border": "#423f55",
+ "foreground": "#75718e"
+ },
+ "hovered": {
+ "background": "#423f55",
+ "border": "#423f55",
+ "foreground": "#75718e"
+ },
+ "pressed": {
+ "background": "#4e4b63",
+ "border": "#423f55",
+ "foreground": "#75718e"
+ },
+ "active": {
+ "background": "#47445b",
+ "border": "#36334a",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#292739",
+ "border": "#353347",
+ "foreground": "#2f2b43"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#4b4860"
+ }
+ },
+ "on": {
+ "default": {
+ "background": "#1d1b2a",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#232132",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#2f2d40",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#403e53",
+ "border": "#504d65",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#1d1b2a",
+ "border": "#1e1c2c",
+ "foreground": "#3b384f"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#3b394e"
+ }
+ },
+ "accent": {
+ "default": {
+ "background": "#2f3739",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "hovered": {
+ "background": "#435255",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "pressed": {
+ "background": "#4e6164",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "active": {
+ "background": "#5d757a",
+ "border": "#6e8f94",
+ "foreground": "#fbfdfd"
+ },
+ "disabled": {
+ "background": "#2f3739",
+ "border": "#3a4446",
+ "foreground": "#85aeb5"
+ },
+ "inverted": {
+ "background": "#fbfdfd",
+ "border": "#171717",
+ "foreground": "#587074"
+ }
+ },
+ "positive": {
+ "default": {
+ "background": "#182e23",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "hovered": {
+ "background": "#254839",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "pressed": {
+ "background": "#2c5645",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "active": {
+ "background": "#356b57",
+ "border": "#40836c",
+ "foreground": "#f9fdfb"
+ },
+ "disabled": {
+ "background": "#182e23",
+ "border": "#1e3b2e",
+ "foreground": "#4ea287"
+ },
+ "inverted": {
+ "background": "#f9fdfb",
+ "border": "#000e00",
+ "foreground": "#326552"
+ }
+ },
+ "warning": {
+ "default": {
+ "background": "#50341a",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "hovered": {
+ "background": "#6d4d2b",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "pressed": {
+ "background": "#7e5a34",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "active": {
+ "background": "#946e41",
+ "border": "#b0854f",
+ "foreground": "#fffcf9"
+ },
+ "disabled": {
+ "background": "#50341a",
+ "border": "#5e4023",
+ "foreground": "#d2a263"
+ },
+ "inverted": {
+ "background": "#fffcf9",
+ "border": "#2c1600",
+ "foreground": "#8e683c"
+ }
+ },
+ "negative": {
+ "default": {
+ "background": "#431820",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "hovered": {
+ "background": "#612834",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "pressed": {
+ "background": "#71303f",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "active": {
+ "background": "#883c4f",
+ "border": "#a44961",
+ "foreground": "#fff9fa"
+ },
+ "disabled": {
+ "background": "#431820",
+ "border": "#52202a",
+ "foreground": "#c75c79"
+ },
+ "inverted": {
+ "background": "#fff9fa",
+ "border": "#230000",
+ "foreground": "#82384a"
+ }
+ }
+ },
+ "middle": {
+ "base": {
+ "default": {
+ "background": "#1d1b2a",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#232132",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#2f2d40",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#403e53",
+ "border": "#504d65",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#1d1b2a",
+ "border": "#1e1c2c",
+ "foreground": "#3b384f"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#3b394e"
+ }
+ },
+ "variant": {
+ "default": {
+ "background": "#1d1b2a",
+ "border": "#232132",
+ "foreground": "#75718e"
+ },
+ "hovered": {
+ "background": "#232132",
+ "border": "#232132",
+ "foreground": "#75718e"
+ },
+ "pressed": {
+ "background": "#2f2d40",
+ "border": "#232132",
+ "foreground": "#75718e"
+ },
+ "active": {
+ "background": "#403e53",
+ "border": "#504d65",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#1d1b2a",
+ "border": "#1e1c2c",
+ "foreground": "#3b384f"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#3b394e"
+ }
+ },
+ "on": {
+ "default": {
+ "background": "#191724",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#1c1a29",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#1d1b2b",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#222031",
+ "border": "#353347",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#191724",
+ "border": "#1a1826",
+ "foreground": "#4e4b63"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#1f1d2e"
+ }
+ },
+ "accent": {
+ "default": {
+ "background": "#2f3739",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "hovered": {
+ "background": "#435255",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "pressed": {
+ "background": "#4e6164",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "active": {
+ "background": "#5d757a",
+ "border": "#6e8f94",
+ "foreground": "#fbfdfd"
+ },
+ "disabled": {
+ "background": "#2f3739",
+ "border": "#3a4446",
+ "foreground": "#85aeb5"
+ },
+ "inverted": {
+ "background": "#fbfdfd",
+ "border": "#171717",
+ "foreground": "#587074"
+ }
+ },
+ "positive": {
+ "default": {
+ "background": "#182e23",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "hovered": {
+ "background": "#254839",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "pressed": {
+ "background": "#2c5645",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "active": {
+ "background": "#356b57",
+ "border": "#40836c",
+ "foreground": "#f9fdfb"
+ },
+ "disabled": {
+ "background": "#182e23",
+ "border": "#1e3b2e",
+ "foreground": "#4ea287"
+ },
+ "inverted": {
+ "background": "#f9fdfb",
+ "border": "#000e00",
+ "foreground": "#326552"
+ }
+ },
+ "warning": {
+ "default": {
+ "background": "#50341a",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "hovered": {
+ "background": "#6d4d2b",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "pressed": {
+ "background": "#7e5a34",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "active": {
+ "background": "#946e41",
+ "border": "#b0854f",
+ "foreground": "#fffcf9"
+ },
+ "disabled": {
+ "background": "#50341a",
+ "border": "#5e4023",
+ "foreground": "#d2a263"
+ },
+ "inverted": {
+ "background": "#fffcf9",
+ "border": "#2c1600",
+ "foreground": "#8e683c"
+ }
+ },
+ "negative": {
+ "default": {
+ "background": "#431820",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "hovered": {
+ "background": "#612834",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "pressed": {
+ "background": "#71303f",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "active": {
+ "background": "#883c4f",
+ "border": "#a44961",
+ "foreground": "#fff9fa"
+ },
+ "disabled": {
+ "background": "#431820",
+ "border": "#52202a",
+ "foreground": "#c75c79"
+ },
+ "inverted": {
+ "background": "#fff9fa",
+ "border": "#230000",
+ "foreground": "#82384a"
+ }
+ }
+ },
+ "highest": {
+ "base": {
+ "default": {
+ "background": "#191724",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#1c1a29",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#1d1b2b",
+ "border": "#1c1a29",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#222031",
+ "border": "#353347",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#191724",
+ "border": "#1a1826",
+ "foreground": "#4e4b63"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#1f1d2e"
+ }
+ },
+ "variant": {
+ "default": {
+ "background": "#191724",
+ "border": "#1c1a29",
+ "foreground": "#75718e"
+ },
+ "hovered": {
+ "background": "#1c1a29",
+ "border": "#1c1a29",
+ "foreground": "#75718e"
+ },
+ "pressed": {
+ "background": "#1d1b2b",
+ "border": "#1c1a29",
+ "foreground": "#75718e"
+ },
+ "active": {
+ "background": "#222031",
+ "border": "#353347",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#191724",
+ "border": "#1a1826",
+ "foreground": "#4e4b63"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#1f1d2e"
+ }
+ },
+ "on": {
+ "default": {
+ "background": "#1d1b2a",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "hovered": {
+ "background": "#232132",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "pressed": {
+ "background": "#2f2d40",
+ "border": "#232132",
+ "foreground": "#e0def4"
+ },
+ "active": {
+ "background": "#403e53",
+ "border": "#504d65",
+ "foreground": "#e0def4"
+ },
+ "disabled": {
+ "background": "#1d1b2a",
+ "border": "#1e1c2c",
+ "foreground": "#3b384f"
+ },
+ "inverted": {
+ "background": "#e0def4",
+ "border": "#191724",
+ "foreground": "#3b394e"
+ }
+ },
+ "accent": {
+ "default": {
+ "background": "#2f3739",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "hovered": {
+ "background": "#435255",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "pressed": {
+ "background": "#4e6164",
+ "border": "#435255",
+ "foreground": "#9cced7"
+ },
+ "active": {
+ "background": "#5d757a",
+ "border": "#6e8f94",
+ "foreground": "#fbfdfd"
+ },
+ "disabled": {
+ "background": "#2f3739",
+ "border": "#3a4446",
+ "foreground": "#85aeb5"
+ },
+ "inverted": {
+ "background": "#fbfdfd",
+ "border": "#171717",
+ "foreground": "#587074"
+ }
+ },
+ "positive": {
+ "default": {
+ "background": "#182e23",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "hovered": {
+ "background": "#254839",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "pressed": {
+ "background": "#2c5645",
+ "border": "#254839",
+ "foreground": "#5dc2a3"
+ },
+ "active": {
+ "background": "#356b57",
+ "border": "#40836c",
+ "foreground": "#f9fdfb"
+ },
+ "disabled": {
+ "background": "#182e23",
+ "border": "#1e3b2e",
+ "foreground": "#4ea287"
+ },
+ "inverted": {
+ "background": "#f9fdfb",
+ "border": "#000e00",
+ "foreground": "#326552"
+ }
+ },
+ "warning": {
+ "default": {
+ "background": "#50341a",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "hovered": {
+ "background": "#6d4d2b",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "pressed": {
+ "background": "#7e5a34",
+ "border": "#6d4d2b",
+ "foreground": "#f5c177"
+ },
+ "active": {
+ "background": "#946e41",
+ "border": "#b0854f",
+ "foreground": "#fffcf9"
+ },
+ "disabled": {
+ "background": "#50341a",
+ "border": "#5e4023",
+ "foreground": "#d2a263"
+ },
+ "inverted": {
+ "background": "#fffcf9",
+ "border": "#2c1600",
+ "foreground": "#8e683c"
+ }
+ },
+ "negative": {
+ "default": {
+ "background": "#431820",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "hovered": {
+ "background": "#612834",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "pressed": {
+ "background": "#71303f",
+ "border": "#612834",
+ "foreground": "#ea6f92"
+ },
+ "active": {
+ "background": "#883c4f",
+ "border": "#a44961",
+ "foreground": "#fff9fa"
+ },
+ "disabled": {
+ "background": "#431820",
+ "border": "#52202a",
+ "foreground": "#c75c79"
+ },
+ "inverted": {
+ "background": "#fff9fa",
+ "border": "#230000",
+ "foreground": "#82384a"
+ }
+ }
+ },
+ "popover_shadow": {
+ "blur": 4,
+ "color": "#00000033",
+ "offset": [
+ 1,
+ 2
+ ]
+ },
+ "modal_shadow": {
+ "blur": 16,
+ "color": "#00000033",
+ "offset": [
+ 0,
+ 2
+ ]
+ },
+ "players": {
+ "0": {
+ "selection": "#9cced73d",
+ "cursor": "#9cced7"
+ },
+ "1": {
+ "selection": "#5dc2a33d",
+ "cursor": "#5dc2a3"
+ },
+ "2": {
+ "selection": "#9d76913d",
+ "cursor": "#9d7691"
+ },
+ "3": {
+ "selection": "#c4a7e63d",
+ "cursor": "#c4a7e6"
+ },
+ "4": {
+ "selection": "#c4a7e63d",
+ "cursor": "#c4a7e6"
+ },
+ "5": {
+ "selection": "#32748f3d",
+ "cursor": "#32748f"
+ },
+ "6": {
+ "selection": "#ea6f923d",
+ "cursor": "#ea6f92"
+ },
+ "7": {
+ "selection": "#f5c1773d",
+ "cursor": "#f5c177"
+ }
+ },
+ "syntax": {
+ "comment": {
+ "color": "#6e6a86"
+ },
+ "operator": {
+ "color": "#31748f"
+ },
+ "punctuation": {
+ "color": "#908caa"
+ },
+ "variable": {
+ "color": "#e0def4"
+ },
+ "string": {
+ "color": "#f6c177"
+ },
+ "type": {
+ "color": "#9ccfd8"
+ },
+ "type.builtin": {
+ "color": "#9ccfd8"
+ },
+ "boolean": {
+ "color": "#ebbcba"
+ },
+ "function": {
+ "color": "#ebbcba"
+ },
+ "keyword": {
+ "color": "#31748f"
+ },
+ "tag": {
+ "color": "#9ccfd8"
+ },
+ "function.method": {
+ "color": "#ebbcba"
+ },
+ "title": {
+ "color": "#f6c177"
+ },
+ "link_text": {
+ "color": "#9ccfd8",
+ "italic": false
+ },
+ "link_uri": {
+ "color": "#ebbcba"
+ }
+ },
+ "color_family": {
+ "neutral": {
+ "low": 11.568627450980392,
+ "high": 91.37254901960785,
+ "range": 79.80392156862746,
+ "scaling_value": 1.2530712530712529
+ },
+ "red": {
+ "low": 6.862745098039216,
+ "high": 100,
+ "range": 93.13725490196079,
+ "scaling_value": 1.0736842105263158
+ },
+ "orange": {
+ "low": 5.490196078431373,
+ "high": 100,
+ "range": 94.50980392156863,
+ "scaling_value": 1.058091286307054
+ },
+ "yellow": {
+ "low": 8.627450980392156,
+ "high": 100,
+ "range": 91.37254901960785,
+ "scaling_value": 1.094420600858369
+ },
+ "green": {
+ "low": 2.7450980392156863,
+ "high": 100,
+ "range": 97.25490196078431,
+ "scaling_value": 1.028225806451613
+ },
+ "cyan": {
+ "low": 0,
+ "high": 100,
+ "range": 100,
+ "scaling_value": 1
+ },
+ "blue": {
+ "low": 9.019607843137255,
+ "high": 100,
+ "range": 90.98039215686275,
+ "scaling_value": 1.0991379310344827
+ },
+ "violet": {
+ "low": 5.490196078431373,
+ "high": 100,
+ "range": 94.50980392156863,
+ "scaling_value": 1.058091286307054
+ },
+ "magenta": {
+ "low": 0,
+ "high": 100,
+ "range": 100,
+ "scaling_value": 1
+ }
+ }
+ }
+ })
+ .unwrap()
+}
@@ -0,0 +1,473 @@
+use crate::{
+ collab_panel::{collab_panel, CollabPanel},
+ theme::theme,
+ themes::rose_pine_dawn,
+};
+use gpui3::{
+ div, img, svg, view, Context, Element, ParentElement, RootView, StyleHelpers, View,
+ ViewContext, WindowContext,
+};
+
+pub struct Workspace {
+ left_panel: View<CollabPanel, Self>,
+ right_panel: View<CollabPanel, Self>,
+}
+
+pub fn workspace(cx: &mut WindowContext) -> RootView<Workspace> {
+ view(cx.entity(|cx| Workspace::new(cx)), Workspace::render)
+}
+
+impl Workspace {
+ fn new(cx: &mut ViewContext<Self>) -> Self {
+ Self {
+ left_panel: collab_panel(cx),
+ right_panel: collab_panel(cx),
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<State = Self> {
+ let theme = rose_pine_dawn();
+ div()
+ .font("Helvetica")
+ .text_base()
+ .size_full()
+ .fill(theme.middle.positive.default.background)
+ .child("Hello world")
+
+ // TODO: Implement style.
+ //.size_full().fill(gpui3::hsla(0.83, 1., 0.5, 1.))
+
+ // TODO: Debug font not font.
+ //.child("Is this thing on?")
+
+ // themed(rose_pine_dawn(), cx, |cx| {
+ // div()
+ // .size_full()
+ // .flex()
+ // .flex_col()
+ // .font("Zed Sans Extended")
+ // .gap_0()
+ // .justify_start()
+ // .items_start()
+ // .text_color(theme.lowest.base.default.foreground)
+ // // .fill(theme.middle.base.default.background)
+ // .fill(gpui3::hsla(0.83, 1., 0.5, 1.))
+ // .child(titlebar(cx))
+ // .child(
+ // div()
+ // .flex_1()
+ // .w_full()
+ // .flex()
+ // .flex_row()
+ // .overflow_hidden()
+ // .child(self.left_panel.clone())
+ // .child(div().h_full().flex_1())
+ // .child(self.right_panel.clone()),
+ // )
+ // .child(statusbar::statusbar(cx))
+ // })
+ }
+}
+
+struct Titlebar;
+
+pub fn titlebar<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let ref mut this = Titlebar;
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .w_full()
+ .h_8()
+ .fill(theme.lowest.base.default.background)
+ .child(this.left_group(cx))
+ .child(this.right_group(cx))
+}
+
+impl Titlebar {
+ fn render<V: 'static + Send + Sync>(
+ &mut self,
+ cx: &mut ViewContext<V>,
+ ) -> impl Element<State = V> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .w_full()
+ .h_8()
+ .fill(theme.lowest.base.default.background)
+ .child(self.left_group(cx))
+ .child(self.right_group(cx))
+ }
+
+ fn left_group<S: 'static + Send + Sync>(
+ &mut self,
+ cx: &mut ViewContext<S>,
+ ) -> impl Element<State = S> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .h_full()
+ .gap_4()
+ .px_2()
+ // === Traffic Lights === //
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_2()
+ .child(
+ div()
+ .w_3()
+ .h_3()
+ .rounded_full()
+ .fill(theme.lowest.positive.default.foreground),
+ )
+ .child(
+ div()
+ .w_3()
+ .h_3()
+ .rounded_full()
+ .fill(theme.lowest.warning.default.foreground),
+ )
+ .child(
+ div()
+ .w_3()
+ .h_3()
+ .rounded_full()
+ .fill(theme.lowest.negative.default.foreground),
+ ),
+ )
+ // === Project Info === //
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_1()
+ .child(
+ div()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .px_2()
+ .rounded_md()
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(div().text_sm().child("project")),
+ )
+ .child(
+ div()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .px_2()
+ .rounded_md()
+ .text_color(theme.lowest.variant.default.foreground)
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(div().text_sm().child("branch")),
+ ),
+ )
+ }
+
+ fn right_group<S: 'static + Send + Sync>(
+ &mut self,
+ cx: &mut ViewContext<S>,
+ ) -> impl Element<State = S> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .h_full()
+ .gap_3()
+ .px_2()
+ // === Actions === //
+ .child(
+ div().child(
+ div().flex().items_center().gap_1().child(
+ div().size_4().flex().items_center().justify_center().child(
+ svg()
+ .path("icons/exit.svg")
+ .size_4()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ ),
+ ),
+ )
+ .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
+ // === Comms === //
+ .child(
+ div().child(
+ div()
+ .flex()
+ .items_center()
+ .gap_px()
+ .child(
+ div()
+ .px_2()
+ .py_1()
+ .rounded_md()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(
+ svg()
+ .path("icons/microphone.svg")
+ .size_3p5()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ )
+ .child(
+ div()
+ .px_2()
+ .py_1()
+ .rounded_md()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(
+ svg()
+ .path("icons/radix/speaker-loud.svg")
+ .size_3p5()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ )
+ .child(
+ div()
+ .px_2()
+ .py_1()
+ .rounded_md()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(
+ svg()
+ .path("icons/radix/desktop.svg")
+ .size_3p5()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ ),
+ ),
+ )
+ .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
+ // User Group
+ .child(
+ div().child(
+ div()
+ .px_1()
+ .py_1()
+ .flex()
+ .items_center()
+ .justify_center()
+ .rounded_md()
+ .gap_0p5()
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(
+ img()
+ .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+ .size_4()
+ .rounded_md()
+ .fill(theme.middle.on.default.foreground),
+ )
+ .child(
+ svg()
+ .path("icons/caret_down_8.svg")
+ .w_2()
+ .h_2()
+ .fill(theme.lowest.variant.default.foreground),
+ ),
+ ),
+ )
+ }
+}
+
+// ================================================================================ //
+
+mod statusbar {
+
+ use super::*;
+
+ pub fn statusbar<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .w_full()
+ .h_8()
+ .fill(theme.lowest.base.default.background)
+ // .child(left_group(cx))
+ // .child(right_group(cx))
+ }
+
+ fn left_group<V: 'static + Send + Sync>(cx: &mut ViewContext<V>) -> impl Element<State = V> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .h_full()
+ .gap_4()
+ .px_2()
+ // === Tools === //
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_1()
+ .child(
+ div()
+ .w_6()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ svg()
+ .path("icons/project.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ )
+ .child(
+ div()
+ .w_6()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ svg()
+ .path("icons/conversations.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ )
+ .child(
+ div()
+ .w_6()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ svg()
+ .path("icons/file_icons/notebook.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.accent.default.foreground),
+ ),
+ ),
+ )
+ // === Diagnostics === //
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_2()
+ .child(
+ div()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .gap_0p5()
+ .px_1()
+ .text_color(theme.lowest.variant.default.foreground)
+ // .hover()
+ // .fill(theme.lowest.base.hovered.background)
+ // .active()
+ // .fill(theme.lowest.base.pressed.background)
+ .child(
+ svg()
+ .path("icons/error.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.negative.default.foreground),
+ )
+ .child(div().text_sm().child("2")),
+ )
+ .child(
+ div()
+ .text_sm()
+ .text_color(theme.lowest.variant.default.foreground)
+ .child("Something is wrong"),
+ ),
+ )
+ }
+
+ fn right_group<S: 'static + Send + Sync>(cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let theme = theme(cx);
+ div()
+ .flex()
+ .items_center()
+ .h_full()
+ .gap_4()
+ .px_2()
+ // === Tools === //
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_1()
+ .child(
+ div()
+ .w_6()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ svg()
+ .path("icons/check_circle.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.base.default.foreground),
+ ),
+ )
+ .child(
+ div()
+ .w_6()
+ .h_full()
+ .flex()
+ .items_center()
+ .justify_center()
+ .child(
+ svg()
+ .path("icons/copilot.svg")
+ .w_4()
+ .h_4()
+ .fill(theme.lowest.accent.default.foreground),
+ ),
+ ),
+ )
+ }
+}
@@ -1,4 +1,7 @@
-use std::sync::Arc;
+use std::{
+ fmt::{self, Debug},
+ sync::Arc,
+};
#[derive(PartialEq, Eq)]
pub enum ArcCow<'a, T: ?Sized> {
@@ -72,3 +75,12 @@ impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
}
}
}
+
+impl<'a, T: ?Sized + Debug> Debug for ArcCow<'a, T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f),
+ ArcCow::Owned(owned) => Debug::fmt(&**owned, f),
+ }
+ }
+}
@@ -139,6 +139,7 @@ tree-sitter-nu.workspace = true
url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4"] }
+owning_ref = "0.4.1"
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }