Compare commits
1 Commits
centralize
...
list-scrol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb1e6846d |
4
.github/workflows/release_nightly.yml
vendored
@@ -2,8 +2,8 @@ name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every day at 1:00pm and 1:00am
|
||||
- cron: "0 1,13 * * *"
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
235
Cargo.lock
generated
@@ -182,7 +182,7 @@ dependencies = [
|
||||
"alacritty_config",
|
||||
"alacritty_config_derive",
|
||||
"base64 0.13.1",
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -200,7 +200,7 @@ dependencies = [
|
||||
"toml 0.7.8",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -293,7 +293,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -303,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -580,7 +580,7 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"rustix 0.37.23",
|
||||
"signal-hook",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1025,9 +1025,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1388,10 +1388,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.84"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1753,7 +1754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.31.0"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1949,7 +1950,6 @@ name = "collab_ui2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update2",
|
||||
"call2",
|
||||
"channel2",
|
||||
"client2",
|
||||
@@ -1958,7 +1958,6 @@ dependencies = [
|
||||
"db2",
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"feedback2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
@@ -1980,12 +1979,10 @@ dependencies = [
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"theme_selector2",
|
||||
"time",
|
||||
"tree-sitter-markdown",
|
||||
"ui2",
|
||||
"util",
|
||||
"vcs_menu2",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
@@ -1994,7 +1991,7 @@ dependencies = [
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rustc-hash",
|
||||
"seahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2050,10 +2047,8 @@ dependencies = [
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"fuzzy2",
|
||||
"go_to_line2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"menu2",
|
||||
"picker2",
|
||||
"project2",
|
||||
"serde",
|
||||
@@ -2088,19 +2083,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-cstr"
|
||||
version = "0.3.0"
|
||||
@@ -2588,7 +2570,7 @@ dependencies = [
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2797,20 +2779,6 @@ dependencies = [
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||
dependencies = [
|
||||
"console",
|
||||
"fuzzy-matcher",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -3056,12 +3024,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
@@ -3140,7 +3102,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3171,7 +3133,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"home",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3283,7 +3245,6 @@ name = "feedback2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.1",
|
||||
"client2",
|
||||
"db2",
|
||||
"editor2",
|
||||
@@ -3303,7 +3264,6 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"theme2",
|
||||
"tree-sitter-markdown",
|
||||
@@ -3380,7 +3340,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3759,15 +3719,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy2"
|
||||
version = "0.1.0"
|
||||
@@ -4030,7 +3981,7 @@ dependencies = [
|
||||
"async-task",
|
||||
"backtrace",
|
||||
"bindgen 0.65.1",
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"block",
|
||||
"cbindgen",
|
||||
"cocoa",
|
||||
@@ -4289,7 +4240,7 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4566,7 +4517,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4623,7 +4574,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"rustix 0.38.14",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4720,6 +4671,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
@@ -5046,7 +5006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5095,18 +5055,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
|
||||
checksum = "b1e6b0bb9ca88d3c5ae88240beb9683821f903b824ee8381ef9ab4e8522fbfa9"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
|
||||
checksum = "b3b3f61e557a617ec6ba36c79431e1f3b5e100d67cfbdb61ed6ef384298af016"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5539,7 +5499,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6199,7 +6159,7 @@ version = "0.10.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -6558,7 +6518,6 @@ dependencies = [
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6720,7 +6679,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"pin-project-lite 0.2.13",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7068,29 +7027,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_symbols2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lsp2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"project2",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.3"
|
||||
@@ -7919,7 +7855,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
@@ -8026,7 +7962,7 @@ dependencies = [
|
||||
"io-lifetimes 1.0.11",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8035,11 +7971,11 @@ version = "0.38.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"errno 0.3.3",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.7",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8152,7 +8088,7 @@ version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8364,7 +8300,6 @@ dependencies = [
|
||||
"menu2",
|
||||
"postage",
|
||||
"project2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -8752,12 +8687,6 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "2.1.2"
|
||||
@@ -8957,7 +8886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9138,7 +9067,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"chrono",
|
||||
@@ -9185,7 +9114,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.1",
|
||||
"bitflags 2.4.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -9263,8 +9192,6 @@ name = "story"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui2",
|
||||
"itertools 0.10.5",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9275,11 +9202,9 @@ dependencies = [
|
||||
"backtrace-on-stack-overflow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"dialoguer",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"itertools 0.11.0",
|
||||
"language2",
|
||||
"log",
|
||||
@@ -9577,7 +9502,7 @@ dependencies = [
|
||||
"fastrand 2.0.0",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix 0.38.14",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9813,7 +9738,6 @@ name = "theme_importer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.4.4",
|
||||
"convert_case 0.6.0",
|
||||
"gpui2",
|
||||
"indexmap 1.9.3",
|
||||
@@ -10017,7 +9941,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.4",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10931,20 +10855,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcs_menu2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fs2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"picker2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@@ -10985,40 +10895,6 @@ dependencies = [
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vim2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"itertools 0.10.5",
|
||||
"language2",
|
||||
"log",
|
||||
"lsp2",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
"project2",
|
||||
"search2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"tokio",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
@@ -11522,7 +11398,6 @@ dependencies = [
|
||||
"theme_selector2",
|
||||
"ui2",
|
||||
"util",
|
||||
"vim2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
@@ -11647,15 +11522,6 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -11795,7 +11661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11988,7 +11854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.118.0"
|
||||
version = "0.117.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -12180,14 +12046,12 @@ dependencies = [
|
||||
"lsp2",
|
||||
"menu2",
|
||||
"node_runtime",
|
||||
"notifications2",
|
||||
"num_cpus",
|
||||
"outline2",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"project_symbols2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
@@ -12250,7 +12114,6 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"vim2",
|
||||
"welcome2",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
|
||||
@@ -90,7 +90,6 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/project_symbols2",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
@@ -123,7 +122,6 @@ members = [
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/vcs_menu2",
|
||||
"crates/workspace2",
|
||||
"crates/welcome",
|
||||
"crates/welcome2",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
||||
|
Before Width: | Height: | Size: 510 B |
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 315 B |
@@ -1 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 552 B |
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minimize-2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 297 B |
@@ -1 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.42503 3.44136C10.0561 3.23654 10.7837 3.2402 11.3792 3.54623C12.7532 4.25224 13.3477 6.07191 12.7946 8C12.5465 8.8649 12.1102 9.70472 11.1861 10.5524C10.262 11.4 8.98034 11.9 8.38571 11.9C8.17269 11.9 8 11.7321 8 11.525C8 11.3179 8.17644 11.15 8.38571 11.15C9.06497 11.15 9.67189 10.7804 10.3906 10.236C10.9406 9.8193 11.3701 9.28633 11.608 8.82191C12.0628 7.93367 12.0782 6.68174 11.3433 6.34901C10.9904 6.73455 10.5295 6.95946 9.97725 6.95946C8.7773 6.95946 8.0701 5.99412 8.10051 5.12009C8.12957 4.28474 8.66032 3.68954 9.42503 3.44136ZM3.42503 3.44136C4.05614 3.23654 4.78366 3.2402 5.37923 3.54623C6.7532 4.25224 7.34766 6.07191 6.79462 8C6.54654 8.8649 6.11019 9.70472 5.1861 10.5524C4.26201 11.4 2.98034 11.9 2.38571 11.9C2.17269 11.9 2 11.7321 2 11.525C2 11.3179 2.17644 11.15 2.38571 11.15C3.06497 11.15 3.67189 10.7804 4.39058 10.236C4.94065 9.8193 5.37014 9.28633 5.60797 8.82191C6.06282 7.93367 6.07821 6.68174 5.3433 6.34901C4.99037 6.73455 4.52948 6.95946 3.97725 6.95946C2.7773 6.95946 2.0701 5.99412 2.10051 5.12009C2.12957 4.28474 2.66032 3.68954 3.42503 3.44136Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>
|
||||
|
Before Width: | Height: | Size: 362 B |
1
assets/icons/split_message.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.81832 0.68179C7.64258 0.506054 7.35766 0.506054 7.18192 0.68179L5.18192 2.68179C5.00619 2.85753 5.00619 3.14245 5.18192 3.31819C5.35766 3.49392 5.64258 3.49392 5.81832 3.31819L7.05012 2.08638L7.05012 5.50023C7.05012 5.74876 7.25159 5.95023 7.50012 5.95023C7.74865 5.95023 7.95012 5.74876 7.95012 5.50023L7.95012 2.08638L9.18192 3.31819C9.35766 3.49392 9.64258 3.49392 9.81832 3.31819C9.99406 3.14245 9.99406 2.85753 9.81832 2.68179L7.81832 0.68179ZM7.95012 12.9136V9.50023C7.95012 9.2517 7.74865 9.05023 7.50012 9.05023C7.25159 9.05023 7.05012 9.2517 7.05012 9.50023V12.9136L5.81832 11.6818C5.64258 11.5061 5.35766 11.5061 5.18192 11.6818C5.00619 11.8575 5.00619 12.1424 5.18192 12.3182L7.18192 14.3182C7.26632 14.4026 7.38077 14.45 7.50012 14.45C7.61947 14.45 7.73393 14.4026 7.81832 14.3182L9.81832 12.3182C9.99406 12.1424 9.99406 11.8575 9.81832 11.6818C9.64258 11.5061 9.35766 11.5061 9.18192 11.6818L7.95012 12.9136ZM1.49994 7.00017C1.2238 7.00017 0.999939 7.22403 0.999939 7.50017C0.999939 7.77631 1.2238 8.00017 1.49994 8.00017L13.4999 8.00017C13.7761 8.00017 13.9999 7.77631 13.9999 7.50017C13.9999 7.22403 13.7761 7.00017 13.4999 7.00017L1.49994 7.00017Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -143,11 +143,7 @@
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
// Amount of indentation for nested items.
|
||||
"indent_size": 20,
|
||||
// Whether to reveal it in the project panel automatically,
|
||||
// when a corresponding project entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true
|
||||
"indent_size": 20
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
|
||||
@@ -14,7 +14,7 @@ use ui::h_stack;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
actions!(ShowErrorMessage);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
@@ -19,19 +19,16 @@ use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
[
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
]
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
);
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -54,9 +54,7 @@ use std::{
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*,
|
||||
utils::{DateTimeType, FormatDistance},
|
||||
ButtonLike, Tab, TabBar, Tooltip,
|
||||
h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip,
|
||||
};
|
||||
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -941,7 +939,7 @@ impl AssistantPanel {
|
||||
this.set_active_editor_index(this.prev_active_editor_index, cx);
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Conversation History", cx))
|
||||
.tooltip(|cx| Tooltip::text("History", cx))
|
||||
}
|
||||
|
||||
fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
|
||||
@@ -957,13 +955,12 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
IconButton::new("split_button", Icon::Snip)
|
||||
IconButton::new("split_button", Icon::SplitMessage)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
if let Some(active_editor) = this.active_editor() {
|
||||
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
|
||||
}
|
||||
}))
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
|
||||
}
|
||||
|
||||
@@ -974,7 +971,6 @@ impl AssistantPanel {
|
||||
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
|
||||
}
|
||||
}))
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
|
||||
}
|
||||
|
||||
@@ -989,7 +985,6 @@ impl AssistantPanel {
|
||||
});
|
||||
}
|
||||
}))
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx))
|
||||
}
|
||||
|
||||
@@ -998,19 +993,15 @@ impl AssistantPanel {
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.new_conversation(cx);
|
||||
}))
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
|
||||
}
|
||||
|
||||
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let zoomed = self.zoomed;
|
||||
IconButton::new("zoom_button", Icon::Maximize)
|
||||
IconButton::new("zoom_button", Icon::MagnifyingGlass)
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.toggle_zoom(&ToggleZoom, cx);
|
||||
}))
|
||||
.selected(zoomed)
|
||||
.selected_icon(Icon::Minimize)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
|
||||
})
|
||||
@@ -1029,19 +1020,10 @@ impl AssistantPanel {
|
||||
this.open_conversation(path.clone(), cx)
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
.full_width()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
|
||||
)
|
||||
.child(Label::new(
|
||||
conversation.mtime.format("%F %I:%M%p").to_string(),
|
||||
))
|
||||
.child(Label::new(conversation.title.clone()))
|
||||
}
|
||||
|
||||
fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
@@ -1130,35 +1112,20 @@ impl Render for AssistantPanel {
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
} else {
|
||||
let header = TabBar::new("assistant_header")
|
||||
.start_child(
|
||||
h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
|
||||
)
|
||||
.children(self.active_editor().map(|editor| {
|
||||
h_stack()
|
||||
.h(rems(Tab::HEIGHT_IN_REMS))
|
||||
.flex_1()
|
||||
.px_2()
|
||||
.child(Label::new(editor.read(cx).title(cx)).into_element())
|
||||
}))
|
||||
.end_child(if self.focus_handle.contains_focused(cx) {
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(h_stack().gap_1().children(self.render_editor_tools(cx)))
|
||||
.child(
|
||||
ui::Divider::vertical()
|
||||
.inset()
|
||||
.color(ui::DividerColor::Border),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(Self::render_plus_button(cx))
|
||||
.child(self.render_zoom_button(cx)),
|
||||
)
|
||||
} else {
|
||||
div()
|
||||
});
|
||||
let title = self
|
||||
.active_editor()
|
||||
.map(|editor| Label::new(editor.read(cx).title(cx)));
|
||||
|
||||
let mut header = h_stack()
|
||||
.child(Self::render_hamburger_button(cx))
|
||||
.children(title);
|
||||
|
||||
if self.focus_handle.contains_focused(cx) {
|
||||
header = header
|
||||
.children(self.render_editor_tools(cx))
|
||||
.child(Self::render_plus_button(cx))
|
||||
.child(self.render_zoom_button(cx));
|
||||
}
|
||||
|
||||
v_stack()
|
||||
.size_full()
|
||||
@@ -1198,6 +1165,8 @@ impl Render for AssistantPanel {
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2282,14 +2251,6 @@ impl ConversationEditor {
|
||||
}
|
||||
Role::System => Label::new("System").color(Color::Warning),
|
||||
})
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Toggle message role",
|
||||
None,
|
||||
"Available roles: You (User), Assistant, System",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
let conversation = conversation.clone();
|
||||
move |_, cx| {
|
||||
@@ -2304,22 +2265,10 @@ impl ConversationEditor {
|
||||
|
||||
h_stack()
|
||||
.id(("message_header", message_id.0))
|
||||
.h_11()
|
||||
.gap_1()
|
||||
.p_1()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(sender)
|
||||
// TODO: Only show this if the message if the message has been sent
|
||||
.child(
|
||||
Label::new(
|
||||
FormatDistance::from_now(DateTimeType::Local(
|
||||
message.sent_at,
|
||||
))
|
||||
.hide_prefix(true)
|
||||
.add_suffix(true)
|
||||
.to_string(),
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(message.sent_at.format("%I:%M%P").to_string()))
|
||||
.children(
|
||||
if let MessageStatus::Error(error) = message.status.clone() {
|
||||
Some(
|
||||
@@ -2480,7 +2429,6 @@ impl ConversationEditor {
|
||||
"current_model",
|
||||
self.conversation.read(cx).model.short_name(),
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.tooltip(move |cx| Tooltip::text("Change Model", cx))
|
||||
.on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
|
||||
}
|
||||
@@ -2494,7 +2442,12 @@ impl ConversationEditor {
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color))
|
||||
Some(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2506,21 +2459,15 @@ impl Render for ConversationEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div()
|
||||
.key_context("ConversationEditor")
|
||||
.size_full()
|
||||
.relative()
|
||||
.capture_action(cx.listener(ConversationEditor::cancel_last_assist))
|
||||
.capture_action(cx.listener(ConversationEditor::save))
|
||||
.capture_action(cx.listener(ConversationEditor::copy))
|
||||
.capture_action(cx.listener(ConversationEditor::cycle_message_role))
|
||||
.on_action(cx.listener(ConversationEditor::assist))
|
||||
.on_action(cx.listener(ConversationEditor::split))
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.pl_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
.child(self.editor.clone())
|
||||
.child(
|
||||
h_stack()
|
||||
.absolute()
|
||||
|
||||
@@ -6,17 +6,15 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
ViewContext, VisualContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::{AppCommitSha, ReleaseChannel};
|
||||
@@ -26,7 +24,13 @@ use workspace::Workspace;
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
//todo!(remove CheckThatAutoUpdaterWorks)
|
||||
actions!(
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
CheckThatAutoUpdaterWorks
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -83,10 +87,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
|
||||
workspace.register_action(|_, action, cx| view_release_notes(action, cx));
|
||||
|
||||
// @nate - code to trigger update notification on launch
|
||||
// todo!("remove this when Nate is done")
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
|
||||
// });
|
||||
@@ -115,10 +116,13 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
//todo!(action)
|
||||
// cx.add_global_action(view_release_notes);
|
||||
// cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use gpui::{
|
||||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
@@ -19,7 +18,6 @@ impl Render for UpdateNotification {
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
v_stack()
|
||||
.on_action(cx.listener(UpdateNotification::dismiss))
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.child(
|
||||
@@ -34,7 +32,7 @@ impl Render for UpdateNotification {
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -52,7 +50,7 @@ impl UpdateNotification {
|
||||
Self { version }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
ViewContext,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
@@ -19,14 +18,16 @@ pub struct Breadcrumbs {
|
||||
pane_focused: bool,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,19 +62,31 @@ impl Render for Breadcrumbs {
|
||||
Label::new("›").into_any_element()
|
||||
});
|
||||
|
||||
let editor = active_item
|
||||
.downcast::<Editor>()
|
||||
.map(|editor| editor.downgrade());
|
||||
|
||||
element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(h_stack().gap_1().children(breadcrumbs))
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
outline::toggle(editor, &outline::Toggle, cx)
|
||||
// We disable the button when the containing pane is not focused:
|
||||
// Because right now all the breadcrumb does is open the outline view, which is an
|
||||
// action which operates on the active editor, clicking the breadcrumbs of another
|
||||
// editor could cause weirdness. I remember that at one point it actually caused a
|
||||
// panic weirdly.
|
||||
//
|
||||
// It might be possible that with changes around how focus is managed that we
|
||||
// might be able to update the active editor to the one with the breadcrumbs
|
||||
// clicked on? That or we could just add a code path for being able to open the
|
||||
// outline for a specific editor. Long term we'd like for it to be an actual
|
||||
// breadcrumb bar so that problem goes away
|
||||
//
|
||||
// — Julia (https://github.com/zed-industries/zed/pull/3505#pullrequestreview-1766198050)
|
||||
.disabled(!self.pane_focused)
|
||||
.on_click(cx.listener(|breadcrumbs, _, cx| {
|
||||
if let Some(workspace) = breadcrumbs.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
outline::toggle(workspace, &outline::Toggle, cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
|
||||
@@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
|
||||
TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
|
||||
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
actions!(SignIn, SignOut, Reconnect);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
TelemetrySettings::register(cx);
|
||||
@@ -1020,116 +1020,91 @@ impl Client {
|
||||
) -> Task<Result<Credentials>> {
|
||||
let http = self.http.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let background = cx.background_executor().clone();
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string =
|
||||
String::try_from(public_key).expect("failed to serialize public key for auth");
|
||||
|
||||
let (open_url_tx, open_url_rx) = oneshot::channel::<String>();
|
||||
cx.update(|cx| {
|
||||
cx.spawn(move |cx| async move {
|
||||
let url = open_url_rx.await?;
|
||||
cx.update(|cx| cx.open_url(&url))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
|
||||
}
|
||||
|
||||
let credentials = background
|
||||
.clone()
|
||||
.spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string = String::try_from(public_key)
|
||||
.expect("failed to serialize public key for auth");
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
|
||||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server =
|
||||
tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
cx.update(|cx| cx.open_url(&url))?;
|
||||
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
|
||||
open_url_tx.send(url).log_err();
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = background
|
||||
.spawn(async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id
|
||||
.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token.ok_or_else(|| {
|
||||
anyhow!("missing access_token parameter")
|
||||
})?,
|
||||
));
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = cx
|
||||
.spawn(|_| async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
})
|
||||
.await?;
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token
|
||||
.ok_or_else(|| anyhow!("missing access_token parameter"))?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
cx.update(|cx| cx.activate(true))?;
|
||||
Ok(credentials)
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use rpc::{
|
||||
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
pub struct FakeServer {
|
||||
peer: Arc<Peer>,
|
||||
@@ -194,7 +195,8 @@ impl FakeServer {
|
||||
client: Arc<Client>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Model<UserStore> {
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client, cx));
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx));
|
||||
assert_eq!(
|
||||
self.receive::<proto::GetUsers>()
|
||||
.await
|
||||
|
||||
@@ -2,12 +2,13 @@ use super::{proto, Client, Status, TypedEnvelope};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{channel::mpsc, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
|
||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task};
|
||||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
use std::sync::{Arc, Weak};
|
||||
use text::ReplicaId;
|
||||
use util::http::HttpClient;
|
||||
use util::TryFutureExt as _;
|
||||
|
||||
pub type UserId = u64;
|
||||
@@ -19,7 +20,7 @@ pub struct ParticipantIndex(pub u32);
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub avatar_uri: SharedString,
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -75,6 +76,7 @@ pub struct UserStore {
|
||||
pending_contact_requests: HashMap<u64, usize>,
|
||||
invite_info: Option<InviteInfo>,
|
||||
client: Weak<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
_maintain_contacts: Task<()>,
|
||||
_maintain_current_user: Task<Result<()>>,
|
||||
}
|
||||
@@ -110,7 +112,11 @@ enum UpdateContacts {
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
|
||||
let rpc_subscriptions = vec![
|
||||
@@ -128,6 +134,7 @@ impl UserStore {
|
||||
invite_info: None,
|
||||
client: Arc::downgrade(&client),
|
||||
update_contacts_tx,
|
||||
http,
|
||||
_maintain_contacts: cx.spawn(|this, mut cx| async move {
|
||||
let _subscriptions = rpc_subscriptions;
|
||||
while let Some(message) = update_contacts_rx.next().await {
|
||||
@@ -438,12 +445,6 @@ impl UserStore {
|
||||
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
|
||||
}
|
||||
|
||||
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
|
||||
self.incoming_contact_requests
|
||||
.iter()
|
||||
.any(|user| user.id == user_id)
|
||||
}
|
||||
|
||||
pub fn respond_to_contact_request(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
@@ -615,14 +616,17 @@ impl UserStore {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let client = self.client.clone();
|
||||
let http = self.http.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(rpc) = client.upgrade() {
|
||||
let response = rpc.request(request).await.context("error loading users")?;
|
||||
let users = response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user))
|
||||
.collect::<Vec<_>>();
|
||||
let users = future::join_all(
|
||||
response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user, http.as_ref())),
|
||||
)
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
for user in &users {
|
||||
@@ -655,11 +659,11 @@ impl UserStore {
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(message: proto::User) -> Arc<Self> {
|
||||
async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
|
||||
Arc::new(User {
|
||||
id: message.id,
|
||||
github_login: message.github_login,
|
||||
avatar_uri: message.avatar_url.into(),
|
||||
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -692,3 +696,25 @@ impl Collaborator {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// todo!("we probably don't need this now that we fetch")
|
||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||
let mut response = http
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("avatar request failed {:?}", response.status()));
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
|
||||
let format = image::guess_format(&body)?;
|
||||
let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
|
||||
Ok(Arc::new(ImageData::new(image)))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.31.0"
|
||||
version = "0.30.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -117,13 +117,12 @@ struct CreateUserResponse {
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
backtrace_hash: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -116,14 +116,12 @@ struct CreateUserResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
backtrace_hash: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -685,9 +685,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
// Client B follows client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.start_following(client_a.peer_id().unwrap(), cx)
|
||||
.unwrap()
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1823,7 +1823,7 @@ async fn test_active_call_events(
|
||||
owner: Arc::new(User {
|
||||
id: client_a.user_id().unwrap(),
|
||||
github_login: "user_a".to_string(),
|
||||
avatar_uri: "avatar_a".into(),
|
||||
avatar: None,
|
||||
}),
|
||||
project_id: project_a_id,
|
||||
worktree_root_names: vec!["a".to_string()],
|
||||
@@ -1841,7 +1841,7 @@ async fn test_active_call_events(
|
||||
owner: Arc::new(User {
|
||||
id: client_b.user_id().unwrap(),
|
||||
github_login: "user_b".to_string(),
|
||||
avatar_uri: "avatar_b".into(),
|
||||
avatar: None,
|
||||
}),
|
||||
project_id: project_b_id,
|
||||
worktree_root_names: vec!["b".to_string()]
|
||||
|
||||
@@ -209,7 +209,7 @@ impl TestServer {
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(cx.executor());
|
||||
|
||||
@@ -22,7 +22,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
||||
# auto_update = { path = "../auto_update" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
call = { package = "call2", path = "../call2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
@@ -32,7 +32,7 @@ collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
feedback = { package = "feedback2", path = "../feedback2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
@@ -46,8 +46,8 @@ rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
|
||||
# theme_selector = { path = "../theme_selector" }
|
||||
# vcs_menu = { path = "../vcs_menu" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{prelude::*, Label};
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
@@ -26,7 +26,7 @@ use workspace::{
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(collab, [Deploy]);
|
||||
actions!(Deploy);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
@@ -253,7 +253,7 @@ impl Item for ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
||||
fn tab_content(&self, _: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
@@ -266,13 +266,7 @@ impl Item for ChannelView {
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label)
|
||||
.color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
Label::new(label).into_any_element()
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
|
||||
@@ -21,7 +21,10 @@ use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
|
||||
use ui::{
|
||||
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
|
||||
IconButton, Label, Tooltip,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -72,7 +75,7 @@ pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
actions!(chat_panel, [ToggleFocus]);
|
||||
actions!(ToggleFocus);
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||
@@ -285,11 +288,7 @@ impl ChatPanel {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.child(self.render_active_channel_messages(cx)),
|
||||
)
|
||||
.child(div().grow().child(self.render_active_channel_messages(cx)))
|
||||
.child(
|
||||
div()
|
||||
.z_index(1)
|
||||
@@ -365,7 +364,13 @@ impl ChatPanel {
|
||||
if !is_continuation {
|
||||
result = result.child(
|
||||
h_stack()
|
||||
.child(Avatar::new(message.sender.avatar_uri.clone()))
|
||||
.children(
|
||||
message
|
||||
.sender
|
||||
.avatar
|
||||
.clone()
|
||||
.map(|avatar| Avatar::data(avatar)),
|
||||
)
|
||||
.child(Label::new(message.sender.github_login.clone()))
|
||||
.child(Label::new(format_timestamp(
|
||||
message.timestamp,
|
||||
@@ -379,18 +384,13 @@ impl ChatPanel {
|
||||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_2()
|
||||
.w_8()
|
||||
.visible_on_hover("")
|
||||
.children(message_id_to_remove.map(|message_id| {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}),
|
||||
)
|
||||
})),
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(render_remove(message_id_to_remove, cx)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
@@ -530,6 +530,18 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
|
||||
if let Some(message_id) = message_id_to_remove {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ChatPanel {}
|
||||
|
||||
impl Render for ChatPanel {
|
||||
@@ -647,7 +659,7 @@ mod tests {
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
sender: Arc::new(client::User {
|
||||
github_login: "fgh".into(),
|
||||
avatar_uri: "avatar_fgh".into(),
|
||||
avatar: None,
|
||||
id: 103,
|
||||
}),
|
||||
nonce: 5,
|
||||
|
||||
@@ -234,7 +234,7 @@ mod tests {
|
||||
user: Arc::new(User {
|
||||
github_login: "a-b".into(),
|
||||
id: 101,
|
||||
avatar_uri: "avatar_a-b".into(),
|
||||
avatar: None,
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
@@ -243,7 +243,7 @@ mod tests {
|
||||
user: Arc::new(User {
|
||||
github_login: "C_D".into(),
|
||||
id: 102,
|
||||
avatar_uri: "avatar_C_D".into(),
|
||||
avatar: None,
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
@@ -275,7 +275,7 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
@@ -5,30 +5,36 @@ use client::{
|
||||
};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, overlay, AppContext, ClipboardItem, DismissEvent, Div, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem};
|
||||
use ui::prelude::*;
|
||||
use util::TryFutureExt;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// Picker::<ChannelModalDelegate>::init(cx);
|
||||
// cx.add_action(ChannelModal::toggle_mode);
|
||||
// cx.add_action(ChannelModal::toggle_member_admin);
|
||||
// cx.add_action(ChannelModal::remove_member);
|
||||
// cx.add_action(ChannelModal::dismiss);
|
||||
// }
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: View<Picker<ChannelModalDelegate>>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
@@ -53,19 +59,25 @@ impl ChannelModal {
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
context_menu: None,
|
||||
members,
|
||||
mode,
|
||||
// context_menu: cx.add_view(|cx| {
|
||||
// let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||
// menu
|
||||
// }),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
let has_focus = picker.focus_handle(cx).contains_focused(cx);
|
||||
|
||||
Self {
|
||||
picker,
|
||||
channel_store,
|
||||
channel_id,
|
||||
has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,19 +123,15 @@ impl ChannelModal {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store
|
||||
.set_channel_visibility(
|
||||
self.channel_id,
|
||||
match selection {
|
||||
Selection::Unselected => ChannelVisibility::Members,
|
||||
Selection::Selected => ChannelVisibility::Public,
|
||||
Selection::Indeterminate => return,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,87 +153,167 @@ impl Render for ChannelModal {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
|
||||
return div();
|
||||
};
|
||||
let channel_name = channel.name.clone();
|
||||
let channel_id = channel.id;
|
||||
let visibility = channel.visibility;
|
||||
let mode = self.picker.read(cx).delegate.mode;
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
// let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
v_stack()
|
||||
.key_context("ChannelModal")
|
||||
.on_action(cx.listener(Self::toggle_mode))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.child(
|
||||
v_stack()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_t(px(8.))
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(IconElement::new(Icon::Hash).size(IconSize::Medium))
|
||||
.child(Label::new(channel_name))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"is-public",
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
)
|
||||
.on_click(cx.listener(Self::set_channel_visiblity)),
|
||||
)
|
||||
.child(Label::new("Public")),
|
||||
)
|
||||
.children(if visibility == ChannelVisibility::Public {
|
||||
Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
|
||||
move |this, _, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
Button::new("manage-members", "Manage Members")
|
||||
.selected(mode == Mode::ManageMembers)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::ManageMembers, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("invite-members", "Invite Members")
|
||||
.selected(mode == Mode::InviteMembers)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::InviteMembers, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(self.picker.clone())
|
||||
// let mode = self.picker.read(cx).delegate().mode;
|
||||
// let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
// return Empty::new().into_any();
|
||||
// };
|
||||
|
||||
// enum InviteMembers {}
|
||||
// enum ManageMembers {}
|
||||
|
||||
// fn render_mode_button<T: 'static>(
|
||||
// mode: Mode,
|
||||
// text: &'static str,
|
||||
// current_mode: Mode,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// let active = mode == current_mode;
|
||||
// MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
// let contained_text = theme.tab_button.style_for(active, state);
|
||||
// Label::new(text, contained_text.text.clone())
|
||||
// .contained()
|
||||
// .with_style(contained_text.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if !active {
|
||||
// this.set_mode(mode, cx);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_visibility(
|
||||
// channel_id: ChannelId,
|
||||
// visibility: ChannelVisibility,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// enum TogglePublic {}
|
||||
|
||||
// if visibility == ChannelVisibility::Members {
|
||||
// return Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Public,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Members,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .with_spacing(14.0)
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
// let style = theme.channel_link.style_for(state);
|
||||
// Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if let Some(channel) =
|
||||
// this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
// {
|
||||
// let item = ClipboardItem::new(channel.link());
|
||||
// cx.write_to_clipboard(item);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.title.container.clone()),
|
||||
// )
|
||||
// .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
// .with_child(Flex::row().with_children([
|
||||
// render_mode_button::<InviteMembers>(
|
||||
// Mode::InviteMembers,
|
||||
// "Invite members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// render_mode_button::<ManageMembers>(
|
||||
// Mode::ManageMembers,
|
||||
// "Manage members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// ]))
|
||||
// .expanded()
|
||||
// .contained()
|
||||
// .with_style(theme.header),
|
||||
// )
|
||||
// .with_child(
|
||||
// ChildView::new(&self.picker, cx)
|
||||
// .contained()
|
||||
// .with_style(theme.body),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_max_height(theme.max_height)
|
||||
// .with_max_width(theme.max_width)
|
||||
// .contained()
|
||||
// .with_style(theme.modal)
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
@@ -245,11 +333,11 @@ pub struct ChannelModalDelegate {
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: Option<(View<ContextMenu>, Subscription)>,
|
||||
// context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
type ListItem = ListItem;
|
||||
type ListItem = Div;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
@@ -329,11 +417,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_member(selected_user.id, cx);
|
||||
self.remove_selected_member(cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
@@ -345,13 +433,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if self.context_menu.is_none() {
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
@@ -360,54 +446,129 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let (user, role) = self.user_at_index(ix)?;
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
None
|
||||
// let full_theme = &theme::current(cx);
|
||||
// let theme = &full_theme.collab_panel.channel_modal;
|
||||
// let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
// let (user, role) = self.user_at_index(ix).unwrap();
|
||||
// let request_status = self.member_status(user.id, cx);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot(h_stack().gap_2().map(|slot| {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => slot
|
||||
.children(
|
||||
if request_status == Some(proto::channel_member::Kind::Invitee) {
|
||||
Some(Label::new("Invited"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.children(match role {
|
||||
Some(ChannelRole::Admin) => Some(Label::new("Admin")),
|
||||
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
|
||||
_ => None,
|
||||
})
|
||||
.child(IconButton::new("ellipsis", Icon::Ellipsis))
|
||||
.children(
|
||||
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
|
||||
Some(
|
||||
overlay()
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
slot.children(Some(Label::new("Invited")))
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {
|
||||
slot.children(Some(Label::new("Member")))
|
||||
}
|
||||
_ => slot,
|
||||
},
|
||||
}
|
||||
})),
|
||||
)
|
||||
// let style = tabbed_modal
|
||||
// .picker
|
||||
// .item
|
||||
// .in_state(selected)
|
||||
// .style_for(mouse_state);
|
||||
|
||||
// let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
// let mut result = Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children({
|
||||
// (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
// || {
|
||||
// Label::new("Invited", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
// .with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
// Some(
|
||||
// Label::new("Admin", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
// Some(
|
||||
// Label::new("Guest", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// })
|
||||
// .with_children({
|
||||
// let svg = match self.mode {
|
||||
// Mode::ManageMembers => Some(
|
||||
// Svg::new("icons/ellipsis.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Mode::InviteMembers => match request_status {
|
||||
// Some(proto::channel_member::Kind::Member) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.invitee_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.button_width)
|
||||
// .with_height(theme.invitee_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.invitee_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
// },
|
||||
// };
|
||||
|
||||
// svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any();
|
||||
|
||||
// if selected {
|
||||
// result = Stack::new()
|
||||
// .with_child(result)
|
||||
// .with_child(
|
||||
// ChildView::new(&self.context_menu, cx)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .right(),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
// result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,20 +602,21 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_user_role(
|
||||
&mut self,
|
||||
user_id: UserId,
|
||||
new_role: ChannelRole,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<()> {
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_role(self.channel_id, user_id, new_role, cx)
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
@@ -465,7 +627,9 @@ impl ChannelModalDelegate {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||
let user_id = user.id;
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
@@ -489,7 +653,7 @@ impl ChannelModalDelegate {
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
picker.focus(cx);
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -522,55 +686,24 @@ impl ChannelModalDelegate {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(
|
||||
&mut self,
|
||||
user: Arc<User>,
|
||||
role: ChannelRole,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) {
|
||||
let user_id = user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
menu = menu.entry("Remove Member", {
|
||||
let picker = picker.clone();
|
||||
move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker.delegate.remove_member(user_id, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let picker = picker.clone();
|
||||
match role {
|
||||
ChannelRole::Admin => {
|
||||
menu = menu.entry("Revoke Admin", move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Member, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
ChannelRole::Member => {
|
||||
menu = menu.entry("Make Admin", move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Admin, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
menu
|
||||
});
|
||||
cx.focus_view(&context_menu);
|
||||
let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
|
||||
picker.delegate.context_menu = None;
|
||||
picker.focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
self.context_menu = Some((context_menu, subscription));
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
// self.context_menu.update(cx, |context_menu, cx| {
|
||||
// context_menu.show(
|
||||
// Default::default(),
|
||||
// AnchorCorner::TopRight,
|
||||
// vec![
|
||||
// ContextMenuItem::action("Remove", RemoveMember),
|
||||
// ContextMenuItem::action(
|
||||
// if role == ChannelRole::Admin {
|
||||
// "Make non-admin"
|
||||
// } else {
|
||||
// "Make admin"
|
||||
// },
|
||||
// ToggleMemberAdmin,
|
||||
// ),
|
||||
// ],
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(unused)]
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
|
||||
@@ -8,12 +7,18 @@ use gpui::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{prelude::*, Avatar, ListItem};
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
//cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: View<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
@@ -24,39 +29,65 @@ impl ContactFinder {
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
};
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx).modal(false));
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
// todo!()
|
||||
// picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContactFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render_mode_button(text: &'static str) -> AnyElement {
|
||||
Label::new(text).into_any_element()
|
||||
}
|
||||
|
||||
v_stack()
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
v_stack()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
// HACK: Prevent the background color from overflowing the parent container.
|
||||
.rounded_t(px(8.))
|
||||
.child(Label::new("Contacts"))
|
||||
.child(h_stack().child(Label::new("Invite new contacts"))),
|
||||
.child(h_stack().children([render_mode_button("Invite new contacts")]))
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.child(self.picker.clone())
|
||||
.w(rems(34.))
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
|
||||
type Element = Div;
|
||||
}
|
||||
|
||||
// impl Modal for ContactFinder {
|
||||
// fn has_focus(&self) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
parent: WeakView<ContactFinder>,
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
@@ -74,8 +105,7 @@ impl FocusableView for ContactFinder {
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
type ListItem = Div;
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
@@ -131,6 +161,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
@@ -153,14 +184,45 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
div()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.children(user.avatar.clone().map(|avatar| img(avatar)))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot::<IconElement>(
|
||||
icon_path.map(|icon_path| IconElement::from_path(icon_path)),
|
||||
),
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
)
|
||||
// Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children(icon_path.map(|icon_path| {
|
||||
// Svg::new(icon_path)
|
||||
// .with_color(button_style.color)
|
||||
// .constrained()
|
||||
// .with_width(button_style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(button_style.container)
|
||||
// .constrained()
|
||||
// .with_width(button_style.button_width)
|
||||
// .with_height(button_style.button_width)
|
||||
// .aligned()
|
||||
// .flex_float()
|
||||
// }))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
use crate::face_pile::FacePile;
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
|
||||
use gpui::{
|
||||
actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent,
|
||||
Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
|
||||
Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
|
||||
VisualContext, WeakView, WindowBounds,
|
||||
actions, canvas, div, point, px, rems, AppContext, Div, Element, Hsla, InteractiveElement,
|
||||
IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled,
|
||||
Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
use std::sync::Arc;
|
||||
use theme::{ActiveTheme, PlayerColors};
|
||||
use ui::{
|
||||
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
|
||||
IconButton, IconElement, Tooltip,
|
||||
IconButton, IconElement, KeyBinding, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
ShareProject,
|
||||
UnshareProject,
|
||||
ToggleUserMenu,
|
||||
ToggleProjectMenu,
|
||||
SwitchBranch
|
||||
]
|
||||
ShareProject,
|
||||
UnshareProject,
|
||||
ToggleUserMenu,
|
||||
ToggleProjectMenu,
|
||||
SwitchBranch
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@@ -52,8 +45,9 @@ pub struct CollabTitlebarItem {
|
||||
user_store: Model<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakView<Workspace>,
|
||||
branch_popover: Option<View<BranchList>>,
|
||||
project_popover: Option<recent_projects::RecentProjects>,
|
||||
//branch_popover: Option<ViewHandle<BranchList>>,
|
||||
//project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||
//user_menu: ViewHandle<ContextMenu>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -74,16 +68,12 @@ impl Render for CollabTitlebarItem {
|
||||
// Set a non-scaling min-height here to ensure the titlebar is
|
||||
// always at least the height of the traffic lights.
|
||||
.min_h(px(32.))
|
||||
.pl_2()
|
||||
.map(|this| {
|
||||
if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
|
||||
this.pl_2()
|
||||
} else {
|
||||
// Use pixels here instead of a rem-based size because the macOS traffic
|
||||
// lights are a static size, and don't scale with the rest of the UI.
|
||||
this.pl(px(72.))
|
||||
}
|
||||
})
|
||||
.when(
|
||||
!matches!(cx.window_bounds(), WindowBounds::Fullscreen),
|
||||
// Use pixels here instead of a rem-based size because the macOS traffic
|
||||
// lights are a static size, and don't scale with the rest of the UI.
|
||||
|s| s.pl(px(68.)),
|
||||
)
|
||||
.bg(cx.theme().colors().title_bar_background)
|
||||
.on_click(|event, cx| {
|
||||
if event.up.click_count == 2 {
|
||||
@@ -169,7 +159,6 @@ impl Render for CollabTitlebarItem {
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.pr_1()
|
||||
.when_some(room, |this, room| {
|
||||
let room = room.read(cx);
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
@@ -238,17 +227,58 @@ impl Render for CollabTitlebarItem {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.map(|el| {
|
||||
let status = self.client.status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
el.child(self.render_user_menu_button(cx))
|
||||
.child(h_stack().px_1p5().map(|this| {
|
||||
if let Some(user) = current_user {
|
||||
this.when_some(user.avatar.clone(), |this, avatar| {
|
||||
// TODO: Finish implementing user menu popover
|
||||
//
|
||||
this.child(
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| menu.header("ADADA"))
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(Avatar::data(avatar))
|
||||
.child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Toggle User Menu", cx)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight),
|
||||
)
|
||||
// this.child(
|
||||
// ButtonLike::new("user-menu")
|
||||
// .child(
|
||||
// h_stack().gap_0p5().child(Avatar::data(avatar)).child(
|
||||
// IconElement::new(Icon::ChevronDown).color(Color::Muted),
|
||||
// ),
|
||||
// )
|
||||
// .style(ButtonStyle::Subtle)
|
||||
// .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
// )
|
||||
})
|
||||
} else {
|
||||
el.children(self.render_connection_status(status, cx))
|
||||
.child(self.render_sign_in_button(cx))
|
||||
.child(self.render_user_menu_button(cx))
|
||||
this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.notify_async_err(&mut cx);
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -290,8 +320,14 @@ impl CollabTitlebarItem {
|
||||
project,
|
||||
user_store,
|
||||
client,
|
||||
branch_popover: None,
|
||||
project_popover: None,
|
||||
// user_menu: cx.add_view(|cx| {
|
||||
// let view_id = cx.view_id();
|
||||
// let mut menu = ContextMenu::new(view_id, cx);
|
||||
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||
// menu
|
||||
// }),
|
||||
// branch_popover: None,
|
||||
// project_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -329,25 +365,11 @@ impl CollabTitlebarItem {
|
||||
|
||||
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||
|
||||
div()
|
||||
.child(
|
||||
Button::new("project_name_trigger", name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_project_menu(&ToggleProjectMenu, cx);
|
||||
})),
|
||||
)
|
||||
.children(self.project_popover.as_ref().map(|popover| {
|
||||
overlay().child(
|
||||
div()
|
||||
.min_w_56()
|
||||
.on_mouse_down_out(cx.listener_for(&popover.picker, |picker, _, cx| {
|
||||
picker.cancel(&Default::default(), cx)
|
||||
}))
|
||||
.child(popover.picker.clone()),
|
||||
)
|
||||
}))
|
||||
div().border().border_color(gpui::red()).child(
|
||||
Button::new("project_name_trigger", name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||
@@ -367,24 +389,23 @@ impl CollabTitlebarItem {
|
||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.child(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
div().border().border_color(gpui::red()).child(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
cx.build_view(|_| {
|
||||
Tooltip::new("Recent Branches")
|
||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
||||
"cmd-b",
|
||||
// todo!() Replace with real action.
|
||||
gpui::NoAction,
|
||||
None,
|
||||
)))
|
||||
.meta("Local branches only")
|
||||
})
|
||||
.on_click(
|
||||
cx.listener(|this, _, cx| this.toggle_vcs_menu(&ToggleVcsMenu, cx)),
|
||||
),
|
||||
)
|
||||
.children(self.render_branches_popover_host()),
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -400,32 +421,42 @@ impl CollabTitlebarItem {
|
||||
current_user: &Arc<User>,
|
||||
) -> Option<FacePile> {
|
||||
let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
|
||||
|
||||
let pile = FacePile::default()
|
||||
.child(
|
||||
Avatar::new(user.avatar_uri.clone())
|
||||
.grayscale(!is_present)
|
||||
.border_color(if is_speaking {
|
||||
gpui::blue()
|
||||
} else if is_muted {
|
||||
gpui::red()
|
||||
} else {
|
||||
Hsla::default()
|
||||
}),
|
||||
)
|
||||
.children(followers.iter().filter_map(|follower_peer_id| {
|
||||
let follower = room
|
||||
.remote_participants()
|
||||
.values()
|
||||
.find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
|
||||
.or_else(|| {
|
||||
(self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
Some(Avatar::new(follower.avatar_uri.clone()))
|
||||
}));
|
||||
|
||||
let mut pile = FacePile::default();
|
||||
pile.extend(
|
||||
user.avatar
|
||||
.clone()
|
||||
.map(|avatar| {
|
||||
div()
|
||||
.child(
|
||||
Avatar::data(avatar.clone())
|
||||
.grayscale(!is_present)
|
||||
.border_color(if is_speaking {
|
||||
gpui::blue()
|
||||
} else if is_muted {
|
||||
gpui::red()
|
||||
} else {
|
||||
Hsla::default()
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.into_iter()
|
||||
.chain(followers.iter().filter_map(|follower_peer_id| {
|
||||
let follower = room
|
||||
.remote_participants()
|
||||
.values()
|
||||
.find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
|
||||
.or_else(|| {
|
||||
(self.client.peer_id() == Some(*follower_peer_id))
|
||||
.then_some(current_user)
|
||||
})?
|
||||
.clone();
|
||||
follower
|
||||
.avatar
|
||||
.clone()
|
||||
.map(|avatar| div().child(Avatar::data(avatar.clone())).into_any_element())
|
||||
})),
|
||||
);
|
||||
Some(pile)
|
||||
}
|
||||
|
||||
@@ -460,177 +491,318 @@ impl CollabTitlebarItem {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_branches_popover_host<'a>(&'a self) -> Option<AnyElement> {
|
||||
self.branch_popover.as_ref().map(|child| {
|
||||
overlay()
|
||||
.child(div().min_w_64().child(child.clone()))
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
// pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
// self.user_menu.update(cx, |user_menu, cx| {
|
||||
// let items = if let Some(_) = self.user_store.read(cx).current_user() {
|
||||
// vec![
|
||||
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
// ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
// ContextMenuItem::separator(),
|
||||
// ContextMenuItem::action(
|
||||
// "Share Feedback",
|
||||
// feedback::feedback_editor::GiveFeedback,
|
||||
// ),
|
||||
// ContextMenuItem::action("Sign Out", SignOut),
|
||||
// ]
|
||||
// } else {
|
||||
// vec![
|
||||
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
// ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
// ContextMenuItem::separator(),
|
||||
// ContextMenuItem::action(
|
||||
// "Share Feedback",
|
||||
// feedback::feedback_editor::GiveFeedback,
|
||||
// ),
|
||||
// ]
|
||||
// };
|
||||
// user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
// });
|
||||
// }
|
||||
|
||||
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.branch_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let Some(view) = build_branch_list(workspace, cx).log_err() else {
|
||||
return;
|
||||
};
|
||||
cx.subscribe(&view, |this, _, _, cx| {
|
||||
this.branch_popover = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.project_popover.take();
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
self.branch_popover = Some(view);
|
||||
}
|
||||
}
|
||||
// fn render_branches_popover_host<'a>(
|
||||
// &'a self,
|
||||
// _theme: &'a theme::Titlebar,
|
||||
// cx: &'a mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// self.branch_popover.as_ref().map(|child| {
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let child = ChildView::new(child, cx);
|
||||
// let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
|
||||
// child
|
||||
// .flex(1., true)
|
||||
// .contained()
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.menu.width)
|
||||
// .with_height(theme.titlebar.menu.height)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, |_, _, _| {})
|
||||
// .on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
// this.branch_popover.take();
|
||||
// cx.emit(());
|
||||
// cx.notify();
|
||||
// })
|
||||
// .contained()
|
||||
// .into_any();
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
// Overlay::new(child)
|
||||
// .with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
// .with_anchor_corner(AnchorCorner::TopLeft)
|
||||
// .with_z_index(999)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .left()
|
||||
// .into_any()
|
||||
// })
|
||||
// }
|
||||
|
||||
pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
if self.project_popover.take().is_none() {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let workspaces = WORKSPACE_DB
|
||||
.recent_workspaces_on_disk()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(_, location)| location)
|
||||
.collect();
|
||||
// fn render_project_popover_host<'a>(
|
||||
// &'a self,
|
||||
// _theme: &'a theme::Titlebar,
|
||||
// cx: &'a mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// self.project_popover.as_ref().map(|child| {
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let child = ChildView::new(child, cx);
|
||||
// let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
|
||||
// child
|
||||
// .flex(1., true)
|
||||
// .contained()
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.menu.width)
|
||||
// .with_height(theme.titlebar.menu.height)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, |_, _, _| {})
|
||||
// .on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
// this.project_popover.take();
|
||||
// cx.emit(());
|
||||
// cx.notify();
|
||||
// })
|
||||
// .into_any();
|
||||
|
||||
let workspace = workspace.clone();
|
||||
this.update(&mut cx, move |this, cx| {
|
||||
let view = RecentProjects::open_popover(workspace, workspaces, cx);
|
||||
// Overlay::new(child)
|
||||
// .with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
// .with_anchor_corner(AnchorCorner::TopLeft)
|
||||
// .with_z_index(999)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .left()
|
||||
// .into_any()
|
||||
// })
|
||||
// }
|
||||
|
||||
cx.subscribe(&view.picker, |this, _, _: &DismissEvent, cx| {
|
||||
this.project_popover = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
// todo!()
|
||||
//this.branch_popover.take();
|
||||
this.project_popover = Some(view);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
// pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
// if self.branch_popover.take().is_none() {
|
||||
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
// let Some(view) =
|
||||
// cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
// cx.subscribe(&view, |this, _, event, cx| {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => {
|
||||
// this.branch_popover = None;
|
||||
// }
|
||||
// }
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
match status {
|
||||
client::Status::ConnectionError
|
||||
| client::Status::ConnectionLost
|
||||
| client::Status::Reauthenticating { .. }
|
||||
| client::Status::Reconnecting { .. }
|
||||
| client::Status::ReconnectionError { .. } => Some(
|
||||
div()
|
||||
.id("disconnected")
|
||||
.bg(gpui::red()) // todo!() @nate
|
||||
.child(IconElement::new(Icon::Disconnected))
|
||||
.tooltip(|cx| Tooltip::text("Disconnected", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
"Please update Zed to Collaborate"
|
||||
}
|
||||
};
|
||||
// cx.notify();
|
||||
// })
|
||||
// .detach();
|
||||
// self.project_popover.take();
|
||||
// cx.focus(&view);
|
||||
// self.branch_popover = Some(view);
|
||||
// }
|
||||
// }
|
||||
|
||||
Some(
|
||||
div()
|
||||
.bg(gpui::red()) // todo!() @nate
|
||||
.child(Button::new("connection-status", label).on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto_update::check(&Default::default(), cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
|
||||
let client = self.client.clone();
|
||||
Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.notify_async_err(&mut cx);
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
// pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
|
||||
// let workspace = self.workspace.clone();
|
||||
// if self.project_popover.take().is_none() {
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// let workspaces = WORKSPACE_DB
|
||||
// .recent_workspaces_on_disk()
|
||||
// .await
|
||||
// .unwrap_or_default()
|
||||
// .into_iter()
|
||||
// .map(|(_, location)| location)
|
||||
// .collect();
|
||||
|
||||
pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
} else {
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
})
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
// let workspace = workspace.clone();
|
||||
// this.update(&mut cx, move |this, cx| {
|
||||
// let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
|
||||
|
||||
// cx.subscribe(&view, |this, _, event, cx| {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => {
|
||||
// this.project_popover = None;
|
||||
// }
|
||||
// }
|
||||
|
||||
// cx.notify();
|
||||
// })
|
||||
// .detach();
|
||||
// cx.focus(&view);
|
||||
// this.branch_popover.take();
|
||||
// this.project_popover = Some(view);
|
||||
// cx.notify();
|
||||
// })
|
||||
// .log_err();
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn render_user_menu_button(
|
||||
// &self,
|
||||
// theme: &Theme,
|
||||
// avatar: Option<Arc<ImageData>>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let tooltip = theme.tooltip.clone();
|
||||
// let user_menu_button_style = if avatar.is_some() {
|
||||
// &theme.titlebar.user_menu.user_menu_button_online
|
||||
// } else {
|
||||
// &theme.titlebar.user_menu.user_menu_button_offline
|
||||
// };
|
||||
|
||||
// let avatar_style = &user_menu_button_style.avatar;
|
||||
// Stack::new()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
|
||||
// let style = user_menu_button_style
|
||||
// .user_menu
|
||||
// .inactive_state()
|
||||
// .style_for(state);
|
||||
|
||||
// let mut dropdown = Flex::row().align_children_center();
|
||||
|
||||
// if let Some(avatar_img) = avatar {
|
||||
// dropdown = dropdown.with_child(Self::render_face(
|
||||
// avatar_img,
|
||||
// *avatar_style,
|
||||
// Color::transparent_black(),
|
||||
// None,
|
||||
// ));
|
||||
// };
|
||||
|
||||
// dropdown
|
||||
// .with_child(
|
||||
// Svg::new("icons/caret_down.svg")
|
||||
// .with_color(user_menu_button_style.icon.color)
|
||||
// .constrained()
|
||||
// .with_width(user_menu_button_style.icon.width)
|
||||
// .contained()
|
||||
// .into_any(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(style.width)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .into_any()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_down(MouseButton::Left, move |_, this, cx| {
|
||||
// this.user_menu.update(cx, |menu, _| menu.delay_cancel());
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.toggle_user_menu(&Default::default(), cx)
|
||||
// })
|
||||
// .with_tooltip::<ToggleUserMenu>(
|
||||
// 0,
|
||||
// "Toggle User Menu".to_owned(),
|
||||
// Some(Box::new(ToggleUserMenu)),
|
||||
// tooltip,
|
||||
// cx,
|
||||
// )
|
||||
// .contained(),
|
||||
// )
|
||||
// .with_child(
|
||||
// ChildView::new(&self.user_menu, cx)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .right(),
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let titlebar = &theme.titlebar;
|
||||
// MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
|
||||
// let style = titlebar.sign_in_button.inactive_state().style_for(state);
|
||||
// Label::new("Sign In", style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// let client = this.client.clone();
|
||||
// cx.app_context()
|
||||
// .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_connection_status(
|
||||
// &self,
|
||||
// status: &client::Status,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// enum ConnectionStatusButton {}
|
||||
|
||||
// let theme = &theme::current(cx).clone();
|
||||
// match status {
|
||||
// client::Status::ConnectionError
|
||||
// | client::Status::ConnectionLost
|
||||
// | client::Status::Reauthenticating { .. }
|
||||
// | client::Status::Reconnecting { .. }
|
||||
// | client::Status::ReconnectionError { .. } => Some(
|
||||
// Svg::new("icons/disconnected.svg")
|
||||
// .with_color(theme.titlebar.offline_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.offline_icon.width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.titlebar.offline_icon.container)
|
||||
// .into_any(),
|
||||
// ),
|
||||
// client::Status::UpgradeRequired => {
|
||||
// let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
// let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
// Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
// Some(AutoUpdateStatus::Installing)
|
||||
// | Some(AutoUpdateStatus::Downloading)
|
||||
// | Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
// Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
// "Please update Zed to Collaborate"
|
||||
// }
|
||||
// };
|
||||
|
||||
// Some(
|
||||
// MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
// Label::new(label, theme.titlebar.outdated_warning.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.titlebar.outdated_warning.container)
|
||||
// .aligned()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, _, cx| {
|
||||
// if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
// if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
// workspace::restart(&Default::default(), cx);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// auto_update::check(&Default::default(), cx);
|
||||
// })
|
||||
// .into_any(),
|
||||
// )
|
||||
// }
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -24,22 +24,18 @@ use settings::Settings;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
);
|
||||
actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
|
||||
vcs_menu::init(cx);
|
||||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
chat_panel::init(cx);
|
||||
notification_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
// cx.add_global_action(toggle_screen_sharing);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use gpui::{
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Default, IntoElement)]
|
||||
pub struct FacePile {
|
||||
pub faces: SmallVec<[AnyElement; 2]>,
|
||||
pub faces: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
impl RenderOnce for FacePile {
|
||||
@@ -17,7 +17,7 @@ impl RenderOnce for FacePile {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div()
|
||||
.z_index((player_count - ix) as u8)
|
||||
.z_index((player_count - ix) as u32)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
@@ -25,8 +25,8 @@ impl RenderOnce for FacePile {
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for FacePile {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.faces
|
||||
impl Extend<AnyElement> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,14 @@ impl IncomingCallNotification {
|
||||
}
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.child(Avatar::new(self.state.call.calling_user.avatar_uri.clone()))
|
||||
.children(
|
||||
self.state
|
||||
.call
|
||||
.calling_user
|
||||
.avatar
|
||||
.as_ref()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(format!(
|
||||
|
||||
@@ -3,12 +3,11 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
img, px, AppContext, Div, ParentElement, Render, Size, Styled, ViewContext, VisualContext,
|
||||
px, AppContext, Div, Element, ParentElement, Render, RenderOnce, Size, Styled, ViewContext,
|
||||
VisualContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{h_stack, prelude::*, v_stack, Button, Label};
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Clickable, Label};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -22,8 +21,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
worktree_root_names,
|
||||
} => {
|
||||
let window_size = Size {
|
||||
width: px(400.),
|
||||
height: px(96.),
|
||||
width: px(380.),
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for screen in cx.displays() {
|
||||
@@ -117,70 +116,61 @@ impl ProjectSharedNotification {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.owner
|
||||
.avatar
|
||||
.clone()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
let this = cx.view().clone();
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").render(cx).on_click({
|
||||
let this = this.clone();
|
||||
move |_, cx| {
|
||||
this.update(cx, |this, cx| this.join(cx));
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
Button::new("dismiss", "Dismiss")
|
||||
.render(cx)
|
||||
.on_click(move |_, cx| {
|
||||
this.update(cx, |this, cx| this.dismiss(cx));
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectSharedNotification {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(
|
||||
theme_settings.ui_font.family.clone(),
|
||||
theme_settings.ui_font_size.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_stack()
|
||||
.font(ui_font)
|
||||
.text_ui()
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.elevation_3(cx)
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(
|
||||
img(self.owner.avatar_uri.clone())
|
||||
.w_16()
|
||||
.h_16()
|
||||
.rounded_full(),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.join(cx);
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.dismiss(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
.bg(gpui::red())
|
||||
.child(self.render_owner())
|
||||
.child(self.render_buttons(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ path = "src/collections.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-support = ["seahash"]
|
||||
|
||||
[dependencies]
|
||||
rustc-hash = "1.1"
|
||||
seahash = { version = "4.1", optional = true }
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashMap<K, V> = FxHashMap<K, V>;
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DeterministicState;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = FxHashSet<T>;
|
||||
impl std::hash::BuildHasher for DeterministicState {
|
||||
type Hasher = seahash::SeaHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
seahash::SeaHasher::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V, DeterministicState>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = std::collections::HashSet<T, DeterministicState>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
@@ -10,7 +23,6 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::any::TypeId;
|
||||
pub use std::collections::*;
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package="language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package="project2", path = "../project2", features = ["test-support"] }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
|
||||
serde_json.workspace = true
|
||||
workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -19,11 +19,10 @@ use util::{
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
actions!(Toggle);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(HitCounts::default());
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ impl CommandPalette {
|
||||
.available_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| {
|
||||
let name = action.name();
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
@@ -101,7 +100,6 @@ pub struct CommandInterceptResult {
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
all_commands: Vec<Command>,
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
@@ -136,7 +134,6 @@ impl CommandPaletteDelegate {
|
||||
) -> Self {
|
||||
Self {
|
||||
command_palette,
|
||||
all_commands: commands.clone(),
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
@@ -169,7 +166,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let mut commands = self.all_commands.clone();
|
||||
let mut commands = self.commands.clone();
|
||||
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
cx.read_global::<HitCounts, _>(|hit_counts, _| {
|
||||
@@ -293,9 +290,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
});
|
||||
let action = command.action;
|
||||
cx.focus(&self.previous_focus_handle);
|
||||
cx.window_context()
|
||||
.spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) })
|
||||
.detach_and_log_err(cx);
|
||||
cx.dispatch_action(action);
|
||||
self.dismissed(cx);
|
||||
}
|
||||
|
||||
@@ -364,9 +359,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use go_to_line::GoToLine;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
@@ -389,6 +382,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
@@ -458,46 +452,12 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-n");
|
||||
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
});
|
||||
editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("go to line: Toggle");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<GoToLine>(cx).is_some())
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("3 enter");
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.focus_handle(cx).is_focused(cx));
|
||||
assert_eq!(
|
||||
editor.selections.last::<Point>(cx).range().start,
|
||||
Point::new(2, 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
menu::init();
|
||||
go_to_line::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -1093,14 +1093,14 @@ mod tests {
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1119,10 +1119,10 @@ mod tests {
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello world".into()
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
@@ -1131,10 +1131,10 @@ mod tests {
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -34,15 +34,12 @@ use util::{
|
||||
};
|
||||
|
||||
actions!(
|
||||
copilot,
|
||||
[
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
]
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
);
|
||||
|
||||
pub fn init(
|
||||
|
||||
@@ -13,10 +13,10 @@ use editor::{
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusHandle,
|
||||
Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
|
||||
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
@@ -43,7 +43,7 @@ use workspace::{
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
actions!(Deploy, ToggleWarnings);
|
||||
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
@@ -109,6 +109,7 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.on_focus_in(cx.listener(Self::focus_in))
|
||||
.on_action(cx.listener(Self::toggle_warnings))
|
||||
.child(child)
|
||||
}
|
||||
@@ -148,11 +149,6 @@ impl ProjectDiagnosticsEditor {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let focus_in_subscription =
|
||||
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
|
||||
|
||||
let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor =
|
||||
@@ -175,17 +171,13 @@ impl ProjectDiagnosticsEditor {
|
||||
summary,
|
||||
workspace,
|
||||
excerpts,
|
||||
focus_handle,
|
||||
focus_handle: cx.focus_handle(),
|
||||
editor,
|
||||
path_states: Default::default(),
|
||||
paths_to_update: HashMap::default(),
|
||||
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
|
||||
current_diagnostics: HashMap::default(),
|
||||
_subscriptions: vec![
|
||||
project_event_subscription,
|
||||
editor_event_subscription,
|
||||
focus_in_subscription,
|
||||
],
|
||||
_subscriptions: vec![project_event_subscription, editor_event_subscription],
|
||||
};
|
||||
this.update_excerpts(None, cx);
|
||||
this
|
||||
@@ -210,7 +202,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
|
||||
if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
|
||||
self.editor.focus_handle(cx).focus(cx)
|
||||
}
|
||||
@@ -641,44 +633,8 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
Some("Project Diagnostics".into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, _detail: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when(self.summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(self.summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement {
|
||||
render_summary(&self.summary)
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@@ -826,6 +782,32 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when(summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(summary.error_count.to_string())),
|
||||
)
|
||||
})
|
||||
.when(summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(summary.warning_count.to_string())),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
|
||||
lhs: &DiagnosticEntry<L>,
|
||||
rhs: &DiagnosticEntry<R>,
|
||||
|
||||
@@ -12,7 +12,6 @@ mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
mod rust_analyzer_ext;
|
||||
pub mod scroll;
|
||||
pub mod selections_collection;
|
||||
|
||||
@@ -301,7 +300,6 @@ actions!(
|
||||
DeleteToEndOfLine,
|
||||
CutToEndOfLine,
|
||||
DuplicateLine,
|
||||
ExpandMacroRecursively,
|
||||
MoveLineUp,
|
||||
MoveLineDown,
|
||||
JoinLines,
|
||||
@@ -427,8 +425,6 @@ pub fn init_settings(cx: &mut AppContext) {
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
|
||||
rust_analyzer_ext::apply_related_actions(cx);
|
||||
cx.add_action(Editor::new_file);
|
||||
cx.add_action(Editor::new_file_in_direction);
|
||||
cx.add_action(Editor::cancel);
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{AppContext, Task, ViewContext};
|
||||
use language::Language;
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{Editor, ExpandMacroRecursively};
|
||||
|
||||
pub fn apply_related_actions(cx: &mut AppContext) {
|
||||
cx.add_async_action(expand_macro_recursively);
|
||||
}
|
||||
|
||||
pub fn expand_macro_recursively(
|
||||
editor: &mut Editor,
|
||||
_: &ExpandMacroRecursively,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
) -> Option<Task<anyhow::Result<()>>> {
|
||||
if editor.selections.count() == 0 {
|
||||
return None;
|
||||
}
|
||||
let project = editor.project.as_ref()?;
|
||||
let workspace = editor.workspace(cx)?;
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
|
||||
let (trigger_anchor, rust_language, server_to_query, buffer) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.into_iter()
|
||||
.filter(|selection| selection.start == selection.end)
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !is_rust_language(&rust_language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, rust_language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, rust_language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||
.into_iter()
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == "rust-analyzer" {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&rust_language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
let project = project.clone();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
|
||||
let expand_macro_task = project.update(cx, |project, cx| {
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
ExpandMacro { position },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Some(cx.spawn(|_, mut cx| async move {
|
||||
let macro_expansion = expand_macro_task.await.context("expand macro")?;
|
||||
if macro_expansion.is_empty() {
|
||||
log::info!("Empty macro expansion for position {position:?}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_buffer(¯o_expansion.expansion, Some(rust_language), cx)
|
||||
})?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
anyhow::Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name().as_ref() == "Rust"
|
||||
}
|
||||
@@ -184,6 +184,7 @@ impl WrapMap {
|
||||
Ok((snapshot, edits)) => {
|
||||
self.snapshot = snapshot;
|
||||
self.edits_since_sync = self.edits_since_sync.compose(&edits);
|
||||
cx.notify();
|
||||
}
|
||||
Err(wrap_task) => {
|
||||
self.background_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -13,7 +13,6 @@ mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
mod rust_analyzer_ext;
|
||||
pub mod scroll;
|
||||
pub mod selections_collection;
|
||||
|
||||
@@ -40,8 +39,8 @@ use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use git::diff_hunk_to_display;
|
||||
use gpui::{
|
||||
actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
|
||||
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
|
||||
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
|
||||
DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures,
|
||||
FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model,
|
||||
MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText,
|
||||
@@ -108,7 +107,7 @@ use ui::{
|
||||
use ui::{prelude::*, IconSize};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent, ItemHandle},
|
||||
item::{ItemEvent, ItemHandle},
|
||||
searchable::SearchEvent,
|
||||
ItemNavHistory, Pane, SplitDirection, ViewId, Workspace,
|
||||
};
|
||||
@@ -186,101 +185,82 @@ pub fn render_parsed_markdown(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectNext {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectPrevious {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectAllMatches {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectToBeginningOfLine {
|
||||
#[serde(default)]
|
||||
stop_at_soft_wraps: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct MovePageUp {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct MovePageDown {
|
||||
#[serde(default)]
|
||||
center_cursor: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct SelectToEndOfLine {
|
||||
#[serde(default)]
|
||||
stop_at_soft_wraps: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ToggleCodeActions {
|
||||
#[serde(default)]
|
||||
pub deployed_from_indicator: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ConfirmCompletion {
|
||||
#[serde(default)]
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ConfirmCodeAction {
|
||||
#[serde(default)]
|
||||
pub item_ix: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct ToggleComments {
|
||||
#[serde(default)]
|
||||
pub advance_downwards: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct FoldAt {
|
||||
pub buffer_row: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
|
||||
pub struct UnfoldAt {
|
||||
pub buffer_row: u32,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
[
|
||||
SelectNext,
|
||||
SelectPrevious,
|
||||
SelectAllMatches,
|
||||
SelectToBeginningOfLine,
|
||||
MovePageUp,
|
||||
MovePageDown,
|
||||
SelectToEndOfLine,
|
||||
ToggleCodeActions,
|
||||
ConfirmCompletion,
|
||||
ConfirmCodeAction,
|
||||
ToggleComments,
|
||||
FoldAt,
|
||||
UnfoldAt
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum InlayId {
|
||||
Suggestion(usize),
|
||||
@@ -297,125 +277,121 @@ impl InlayId {
|
||||
}
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
Backspace,
|
||||
Cancel,
|
||||
ConfirmRename,
|
||||
ContextMenuFirst,
|
||||
ContextMenuLast,
|
||||
ContextMenuNext,
|
||||
ContextMenuPrev,
|
||||
ConvertToKebabCase,
|
||||
ConvertToLowerCamelCase,
|
||||
ConvertToLowerCase,
|
||||
ConvertToSnakeCase,
|
||||
ConvertToTitleCase,
|
||||
ConvertToUpperCamelCase,
|
||||
ConvertToUpperCase,
|
||||
Copy,
|
||||
CopyHighlightJson,
|
||||
CopyPath,
|
||||
CopyRelativePath,
|
||||
Cut,
|
||||
CutToEndOfLine,
|
||||
Delete,
|
||||
DeleteLine,
|
||||
DeleteToBeginningOfLine,
|
||||
DeleteToEndOfLine,
|
||||
DeleteToNextSubwordEnd,
|
||||
DeleteToNextWordEnd,
|
||||
DeleteToPreviousSubwordStart,
|
||||
DeleteToPreviousWordStart,
|
||||
DuplicateLine,
|
||||
ExpandMacroRecursively,
|
||||
FindAllReferences,
|
||||
Fold,
|
||||
FoldSelectedRanges,
|
||||
Format,
|
||||
GoToDefinition,
|
||||
GoToDefinitionSplit,
|
||||
GoToDiagnostic,
|
||||
GoToHunk,
|
||||
GoToPrevDiagnostic,
|
||||
GoToPrevHunk,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
HalfPageDown,
|
||||
HalfPageUp,
|
||||
Hover,
|
||||
Indent,
|
||||
JoinLines,
|
||||
LineDown,
|
||||
LineUp,
|
||||
MoveDown,
|
||||
MoveLeft,
|
||||
MoveLineDown,
|
||||
MoveLineUp,
|
||||
MoveRight,
|
||||
MoveToBeginning,
|
||||
MoveToBeginningOfLine,
|
||||
MoveToEnclosingBracket,
|
||||
MoveToEnd,
|
||||
MoveToEndOfLine,
|
||||
MoveToEndOfParagraph,
|
||||
MoveToNextSubwordEnd,
|
||||
MoveToNextWordEnd,
|
||||
MoveToPreviousSubwordStart,
|
||||
MoveToPreviousWordStart,
|
||||
MoveToStartOfParagraph,
|
||||
MoveUp,
|
||||
Newline,
|
||||
NewlineAbove,
|
||||
NewlineBelow,
|
||||
NextScreen,
|
||||
OpenExcerpts,
|
||||
Outdent,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Paste,
|
||||
Redo,
|
||||
RedoSelection,
|
||||
Rename,
|
||||
RestartLanguageServer,
|
||||
RevealInFinder,
|
||||
ReverseLines,
|
||||
ScrollCursorBottom,
|
||||
ScrollCursorCenter,
|
||||
ScrollCursorTop,
|
||||
SelectAll,
|
||||
SelectDown,
|
||||
SelectLargerSyntaxNode,
|
||||
SelectLeft,
|
||||
SelectLine,
|
||||
SelectRight,
|
||||
SelectSmallerSyntaxNode,
|
||||
SelectToBeginning,
|
||||
SelectToEnd,
|
||||
SelectToEndOfParagraph,
|
||||
SelectToNextSubwordEnd,
|
||||
SelectToNextWordEnd,
|
||||
SelectToPreviousSubwordStart,
|
||||
SelectToPreviousWordStart,
|
||||
SelectToStartOfParagraph,
|
||||
SelectUp,
|
||||
ShowCharacterPalette,
|
||||
ShowCompletions,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleInlayHints,
|
||||
ToggleSoftWrap,
|
||||
Transpose,
|
||||
Undo,
|
||||
UndoSelection,
|
||||
UnfoldLines,
|
||||
]
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
Backspace,
|
||||
Cancel,
|
||||
ConfirmRename,
|
||||
ContextMenuFirst,
|
||||
ContextMenuLast,
|
||||
ContextMenuNext,
|
||||
ContextMenuPrev,
|
||||
ConvertToKebabCase,
|
||||
ConvertToLowerCamelCase,
|
||||
ConvertToLowerCase,
|
||||
ConvertToSnakeCase,
|
||||
ConvertToTitleCase,
|
||||
ConvertToUpperCamelCase,
|
||||
ConvertToUpperCase,
|
||||
Copy,
|
||||
CopyHighlightJson,
|
||||
CopyPath,
|
||||
CopyRelativePath,
|
||||
Cut,
|
||||
CutToEndOfLine,
|
||||
Delete,
|
||||
DeleteLine,
|
||||
DeleteToBeginningOfLine,
|
||||
DeleteToEndOfLine,
|
||||
DeleteToNextSubwordEnd,
|
||||
DeleteToNextWordEnd,
|
||||
DeleteToPreviousSubwordStart,
|
||||
DeleteToPreviousWordStart,
|
||||
DuplicateLine,
|
||||
FindAllReferences,
|
||||
Fold,
|
||||
FoldSelectedRanges,
|
||||
Format,
|
||||
GoToDefinition,
|
||||
GoToDefinitionSplit,
|
||||
GoToDiagnostic,
|
||||
GoToHunk,
|
||||
GoToPrevDiagnostic,
|
||||
GoToPrevHunk,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
HalfPageDown,
|
||||
HalfPageUp,
|
||||
Hover,
|
||||
Indent,
|
||||
JoinLines,
|
||||
LineDown,
|
||||
LineUp,
|
||||
MoveDown,
|
||||
MoveLeft,
|
||||
MoveLineDown,
|
||||
MoveLineUp,
|
||||
MoveRight,
|
||||
MoveToBeginning,
|
||||
MoveToBeginningOfLine,
|
||||
MoveToEnclosingBracket,
|
||||
MoveToEnd,
|
||||
MoveToEndOfLine,
|
||||
MoveToEndOfParagraph,
|
||||
MoveToNextSubwordEnd,
|
||||
MoveToNextWordEnd,
|
||||
MoveToPreviousSubwordStart,
|
||||
MoveToPreviousWordStart,
|
||||
MoveToStartOfParagraph,
|
||||
MoveUp,
|
||||
Newline,
|
||||
NewlineAbove,
|
||||
NewlineBelow,
|
||||
NextScreen,
|
||||
OpenExcerpts,
|
||||
Outdent,
|
||||
PageDown,
|
||||
PageUp,
|
||||
Paste,
|
||||
Redo,
|
||||
RedoSelection,
|
||||
Rename,
|
||||
RestartLanguageServer,
|
||||
RevealInFinder,
|
||||
ReverseLines,
|
||||
ScrollCursorBottom,
|
||||
ScrollCursorCenter,
|
||||
ScrollCursorTop,
|
||||
SelectAll,
|
||||
SelectDown,
|
||||
SelectLargerSyntaxNode,
|
||||
SelectLeft,
|
||||
SelectLine,
|
||||
SelectRight,
|
||||
SelectSmallerSyntaxNode,
|
||||
SelectToBeginning,
|
||||
SelectToEnd,
|
||||
SelectToEndOfParagraph,
|
||||
SelectToNextSubwordEnd,
|
||||
SelectToNextWordEnd,
|
||||
SelectToPreviousSubwordStart,
|
||||
SelectToPreviousWordStart,
|
||||
SelectToStartOfParagraph,
|
||||
SelectUp,
|
||||
ShowCharacterPalette,
|
||||
ShowCompletions,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleInlayHints,
|
||||
ToggleSoftWrap,
|
||||
Transpose,
|
||||
Undo,
|
||||
UndoSelection,
|
||||
UnfoldLines,
|
||||
);
|
||||
|
||||
enum DocumentHighlightRead {}
|
||||
@@ -1250,7 +1226,6 @@ impl CompletionsMenu {
|
||||
let documentation_label =
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
Some(SharedString::from(text.clone()))
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1924,9 +1899,9 @@ impl Editor {
|
||||
self.buffer.read(cx).replica_id()
|
||||
}
|
||||
|
||||
pub fn leader_peer_id(&self) -> Option<PeerId> {
|
||||
self.leader_peer_id
|
||||
}
|
||||
// pub fn leader_peer_id(&self) -> Option<PeerId> {
|
||||
// self.leader_peer_id
|
||||
// }
|
||||
|
||||
pub fn buffer(&self) -> &Model<MultiBuffer> {
|
||||
&self.buffer
|
||||
@@ -2172,6 +2147,7 @@ impl Editor {
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
cx.emit(EditorEvent::SelectionsChanged { local });
|
||||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
|
||||
if self.selections.disjoint_anchors().len() == 1 {
|
||||
cx.emit(SearchEvent::ActiveMatchChanged)
|
||||
@@ -4228,18 +4204,16 @@ impl Editor {
|
||||
) -> Option<IconButton> {
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(|editor, e, cx| {
|
||||
IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener(
|
||||
|editor, e, cx| {
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
},
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -4262,7 +4236,11 @@ impl Editor {
|
||||
fold_data
|
||||
.map(|(fold_status, buffer_row, active)| {
|
||||
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
|
||||
IconButton::new(ix as usize, ui::Icon::ChevronDown)
|
||||
let icon = match fold_status {
|
||||
FoldStatus::Folded => ui::Icon::ChevronRight,
|
||||
FoldStatus::Foldable => ui::Icon::ChevronDown,
|
||||
};
|
||||
IconButton::new(ix as usize, icon)
|
||||
.on_click(cx.listener(move |editor, e, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
@@ -4272,10 +4250,6 @@ impl Editor {
|
||||
}
|
||||
}))
|
||||
.icon_color(ui::Color::Muted)
|
||||
.icon_size(ui::IconSize::Small)
|
||||
.selected(fold_status == FoldStatus::Folded)
|
||||
.selected_icon(ui::Icon::ChevronRight)
|
||||
.size(ui::ButtonSize::None)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
@@ -8302,9 +8276,7 @@ impl Editor {
|
||||
self.style.as_ref()
|
||||
}
|
||||
|
||||
// Called by the element. This method is not designed to be called outside of the editor
|
||||
// element's layout code because it does not notify when rewrapping is computed synchronously.
|
||||
pub(crate) fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
|
||||
pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
}
|
||||
@@ -8710,13 +8682,13 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_searchable(&mut self, searchable: bool) {
|
||||
self.searchable = searchable;
|
||||
}
|
||||
// pub fn set_searchable(&mut self, searchable: bool) {
|
||||
// self.searchable = searchable;
|
||||
// }
|
||||
|
||||
pub fn searchable(&self) -> bool {
|
||||
self.searchable
|
||||
}
|
||||
// pub fn searchable(&self) -> bool {
|
||||
// self.searchable
|
||||
// }
|
||||
|
||||
fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
@@ -9100,7 +9072,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// todo!()
|
||||
// let blurred_event = EditorBlurred(cx.handle());
|
||||
// cx.emit_global(blurred_event);
|
||||
self.blink_manager.update(cx, BlinkManager::disable);
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
|
||||
@@ -9284,6 +9259,10 @@ pub enum EditorEvent {
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct EditorFocused(pub View<Editor>);
|
||||
pub struct EditorBlurred(pub View<Editor>);
|
||||
pub struct EditorReleased(pub WeakView<Editor>);
|
||||
|
||||
impl EventEmitter<EditorEvent> for Editor {}
|
||||
|
||||
impl FocusableView for Editor {
|
||||
@@ -9299,20 +9278,20 @@ impl Render for Editor {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
line_height: relative(1.).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
},
|
||||
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
@@ -9340,6 +9319,7 @@ impl Render for Editor {
|
||||
scrollbar_width: px(12.),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
diagnostic_style: cx.theme().diagnostic_style(),
|
||||
// TODO kb find `HighlightStyle` usages
|
||||
// todo!("what about the rest of the highlight style parts?")
|
||||
inlays_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().hint),
|
||||
@@ -9741,8 +9721,12 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
};
|
||||
highlighted_lines.push(line);
|
||||
}
|
||||
let message = diagnostic.message;
|
||||
Arc::new(move |cx: &mut BlockContext| {
|
||||
let message = message.clone();
|
||||
let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into();
|
||||
let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||
|
||||
// TODO: Nate: We should tint the background of the block with the severity color
|
||||
// We need to extend the theme before we can do this
|
||||
v_stack()
|
||||
@@ -9752,6 +9736,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
.bg(gpui::red())
|
||||
.children(highlighted_lines.iter().map(|(line, highlights)| {
|
||||
let group_id = cx.block_id.to_string();
|
||||
|
||||
h_stack()
|
||||
.group(group_id.clone())
|
||||
.gap_2()
|
||||
@@ -9760,20 +9745,19 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
.px_1p5()
|
||||
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
||||
.child(
|
||||
div().z_index(1).child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id)
|
||||
.on_click(cx.listener({
|
||||
let message = diagnostic.message.clone();
|
||||
move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()))
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.invisible()
|
||||
.group_hover(group_id, |style| style.visible())
|
||||
.child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
)
|
||||
}))
|
||||
.into_any_element()
|
||||
|
||||
@@ -26,7 +26,7 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||
pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
|
||||
pub const HOVER_POPOVER_GAP: Pixels = px(10.);
|
||||
|
||||
actions!(editor, [Hover]);
|
||||
actions!(Hover);
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
|
||||
@@ -580,7 +580,7 @@ impl Item for Editor {
|
||||
Some(path.to_string_lossy().to_string().into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
||||
fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
|
||||
let description = detail.and_then(|detail| {
|
||||
@@ -597,11 +597,7 @@ impl Item for Editor {
|
||||
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Label::new(self.title(cx).to_string()).color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.when_some(description, |this, description| {
|
||||
this.child(Label::new(description).color(Color::Muted))
|
||||
})
|
||||
@@ -1189,7 +1185,7 @@ impl CursorPosition {
|
||||
impl Render for CursorPosition {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().when_some(self.position, |el, position| {
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{Context, Model, View, ViewContext, VisualContext, WindowContext};
|
||||
use language::Language;
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{element::register_action, Editor, ExpandMacroRecursively};
|
||||
|
||||
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
|
||||
let is_rust_related = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
.iter()
|
||||
.any(|b| match b.read(cx).language() {
|
||||
Some(l) => is_rust_language(l),
|
||||
None => false,
|
||||
})
|
||||
});
|
||||
|
||||
if is_rust_related {
|
||||
register_action(editor, cx, expand_macro_recursively);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_macro_recursively(
|
||||
editor: &mut Editor,
|
||||
_: &ExpandMacroRecursively,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) {
|
||||
if editor.selections.count() == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
|
||||
let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.into_iter()
|
||||
.filter(|selection| selection.start == selection.end)
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !is_rust_language(&rust_language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, rust_language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, rust_language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||
.into_iter()
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == "rust-analyzer" {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&rust_language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.clone();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
|
||||
let expand_macro_task = project.update(cx, |project, cx| {
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
ExpandMacro { position },
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let macro_expansion = expand_macro_task.await.context("expand macro")?;
|
||||
if macro_expansion.is_empty() {
|
||||
log::info!("Empty macro expansion for position {position:?}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let buffer = project.update(&mut cx, |project, cx| {
|
||||
project.create_buffer(¯o_expansion.expansion, Some(rust_language), cx)
|
||||
})??;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let buffer = cx.build_model(|cx| {
|
||||
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name().as_ref() == "Rust"
|
||||
}
|
||||
@@ -136,7 +136,6 @@ pub struct ScrollManager {
|
||||
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
dragging_scrollbar: bool,
|
||||
visible_line_count: Option<f32>,
|
||||
}
|
||||
|
||||
@@ -149,7 +148,6 @@ impl ScrollManager {
|
||||
autoscroll_request: None,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
dragging_scrollbar: false,
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
}
|
||||
@@ -280,17 +278,6 @@ impl ScrollManager {
|
||||
self.autoscroll_request.is_some()
|
||||
}
|
||||
|
||||
pub fn is_dragging_scrollbar(&self) -> bool {
|
||||
self.dragging_scrollbar
|
||||
}
|
||||
|
||||
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
|
||||
if dragging != self.dragging_scrollbar {
|
||||
self.dragging_scrollbar = dragging;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
|
||||
if max < self.anchor.offset.x {
|
||||
self.anchor.offset.x = max;
|
||||
|
||||
@@ -170,10 +170,6 @@ impl<'a> EditorTestContext<'a> {
|
||||
keystrokes_under_test_handle
|
||||
}
|
||||
|
||||
pub fn run_until_parked(&mut self) {
|
||||
self.cx.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(self.buffer_text(), unmarked_text);
|
||||
|
||||
@@ -23,7 +23,7 @@ impl SystemSpecs {
|
||||
pub fn new(cx: &AppContext) -> Self {
|
||||
let platform = cx.platform();
|
||||
let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
|
||||
let release_channel = cx.global::<ReleaseChannel>().display_name();
|
||||
let release_channel = cx.global::<ReleaseChannel>().dev_name();
|
||||
let os_name = platform.os_name();
|
||||
let system = System::new_all();
|
||||
let memory = system.total_memory();
|
||||
|
||||
@@ -18,6 +18,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
regex.workspace = true
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
@@ -25,20 +26,16 @@ ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2"}
|
||||
|
||||
bitflags = "2.4.1"
|
||||
human_bytes = "0.4.1"
|
||||
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
futures.workspace = true
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
@@ -5,18 +5,15 @@ use workspace::Workspace;
|
||||
pub mod deploy_feedback_button;
|
||||
pub mod feedback_modal;
|
||||
|
||||
actions!(feedback, [GiveFeedback, SubmitFeedback]);
|
||||
actions!(GiveFeedback, SubmitFeedback);
|
||||
|
||||
mod system_specs;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
CopySystemSpecsIntoClipboard,
|
||||
FileBugReport,
|
||||
RequestFeature,
|
||||
OpenZedCommunityRepo
|
||||
]
|
||||
CopySystemSpecsIntoClipboard,
|
||||
FileBugReport,
|
||||
RequestFeature,
|
||||
OpenZedCommunityRepo
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use std::{ops::RangeInclusive, sync::Arc, time::Duration};
|
||||
use std::{ops::RangeInclusive, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use bitflags::bitflags;
|
||||
use anyhow::bail;
|
||||
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
|
||||
div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Model, PromptLevel, Render, Task, View, ViewContext,
|
||||
};
|
||||
use isahc::Request;
|
||||
use language::Buffer;
|
||||
@@ -21,20 +20,9 @@ use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
|
||||
|
||||
// For UI testing purposes
|
||||
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
|
||||
const SEND_TIME_IN_DEV_MODE: Duration = Duration::from_secs(2);
|
||||
|
||||
// Temporary, until tests are in place
|
||||
#[cfg(debug_assertions)]
|
||||
const DEV_MODE: bool = true;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEV_MODE: bool = false;
|
||||
|
||||
const DATABASE_KEY_NAME: &str = "email_address";
|
||||
const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
|
||||
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
|
||||
"Feedback failed to submit, see error log for details.";
|
||||
|
||||
@@ -49,33 +37,12 @@ struct FeedbackRequestBody<'a> {
|
||||
token: &'a str,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct InvalidStateFlags: u8 {
|
||||
const EmailAddress = 0b00000001;
|
||||
const CharacterCount = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CannotSubmitReason {
|
||||
InvalidState { flags: InvalidStateFlags },
|
||||
AwaitingSubmission,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SubmissionState {
|
||||
CanSubmit,
|
||||
CannotSubmit { reason: CannotSubmitReason },
|
||||
}
|
||||
|
||||
pub struct FeedbackModal {
|
||||
system_specs: SystemSpecs,
|
||||
feedback_editor: View<Editor>,
|
||||
email_address_editor: View<Editor>,
|
||||
submission_state: Option<SubmissionState>,
|
||||
dismiss_modal: bool,
|
||||
character_count: i32,
|
||||
character_count: usize,
|
||||
pending_submission: bool,
|
||||
}
|
||||
|
||||
impl FocusableView for FeedbackModal {
|
||||
@@ -86,30 +53,16 @@ impl FocusableView for FeedbackModal {
|
||||
impl EventEmitter<DismissEvent> for FeedbackModal {}
|
||||
|
||||
impl ModalView for FeedbackModal {
|
||||
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.dismiss_modal {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) -> Task<bool> {
|
||||
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
|
||||
|
||||
if !has_feedback {
|
||||
return true;
|
||||
return cx.spawn(|_, _| async { true });
|
||||
}
|
||||
|
||||
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if answer.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.dismiss_modal = true;
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
false
|
||||
cx.spawn(|_, _| async { answer.await.ok() == Some(0) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,39 +116,41 @@ impl FeedbackModal {
|
||||
editor
|
||||
});
|
||||
|
||||
// Moved here because providing it inline breaks rustfmt
|
||||
let placeholder_text =
|
||||
"You can use markdown to organize your feedback with code and links.";
|
||||
|
||||
let feedback_editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
|
||||
editor.set_placeholder_text(placeholder_text, cx);
|
||||
editor.set_placeholder_text(
|
||||
"You can use markdown to organize your feedback wiht add code and links, or organize feedback.",
|
||||
cx,
|
||||
);
|
||||
// editor.set_show_gutter(false, cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
|
||||
if *event == EditorEvent::Edited {
|
||||
this.character_count = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("Feedback editor is never a multi-buffer")
|
||||
.read(cx)
|
||||
.len() as i32;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
cx.subscribe(
|
||||
&feedback_editor,
|
||||
|this, editor, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::Edited => {
|
||||
this.character_count = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("Feedback editor is never a multi-buffer")
|
||||
.read(cx)
|
||||
.len();
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
system_specs: system_specs.clone(),
|
||||
feedback_editor,
|
||||
email_address_editor,
|
||||
submission_state: None,
|
||||
dismiss_modal: false,
|
||||
pending_submission: false,
|
||||
character_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -216,81 +171,57 @@ impl FeedbackModal {
|
||||
if answer == Some(0) {
|
||||
match email.clone() {
|
||||
Some(email) => {
|
||||
KEY_VALUE_STORE
|
||||
let _ = KEY_VALUE_STORE
|
||||
.write_kvp(DATABASE_KEY_NAME.to_string(), email)
|
||||
.await
|
||||
.ok();
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
KEY_VALUE_STORE
|
||||
let _ = KEY_VALUE_STORE
|
||||
.delete_kvp(DATABASE_KEY_NAME.to_string())
|
||||
.await
|
||||
.ok();
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission,
|
||||
});
|
||||
cx.notify();
|
||||
this.update(&mut cx, |feedback_editor, cx| {
|
||||
feedback_editor.set_pending_submission(true, cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
let res =
|
||||
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.dismiss_modal = true;
|
||||
cx.notify();
|
||||
cx.emit(DismissEvent)
|
||||
if let Err(error) =
|
||||
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await
|
||||
{
|
||||
log::error!("{}", error);
|
||||
this.update(&mut cx, |feedback_editor, cx| {
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
FEEDBACK_SUBMISSION_ERROR_TEXT,
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("{}", error);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
FEEDBACK_SUBMISSION_ERROR_TEXT,
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
this.submission_state = Some(SubmissionState::CanSubmit);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
.detach();
|
||||
feedback_editor.set_pending_submission(false, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) {
|
||||
self.pending_submission = pending_submission;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
async fn submit_feedback(
|
||||
feedback_text: &str,
|
||||
email: Option<String>,
|
||||
zed_client: Arc<Client>,
|
||||
system_specs: SystemSpecs,
|
||||
) -> anyhow::Result<()> {
|
||||
if DEV_MODE {
|
||||
smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
|
||||
|
||||
if SEND_SUCCESS_IN_DEV_MODE {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Error submitting feedback"));
|
||||
}
|
||||
}
|
||||
|
||||
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
|
||||
let telemetry = zed_client.telemetry();
|
||||
let metrics_id = telemetry.metrics_id();
|
||||
@@ -320,67 +251,7 @@ impl FeedbackModal {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_submission_state(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.awaiting_submission() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut invalid_state_flags = InvalidStateFlags::empty();
|
||||
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
|
||||
if !valid_email_address {
|
||||
invalid_state_flags |= InvalidStateFlags::EmailAddress;
|
||||
}
|
||||
|
||||
if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
|
||||
invalid_state_flags |= InvalidStateFlags::CharacterCount;
|
||||
}
|
||||
|
||||
if invalid_state_flags.is_empty() {
|
||||
self.submission_state = Some(SubmissionState::CanSubmit);
|
||||
} else {
|
||||
self.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState {
|
||||
flags: invalid_state_flags,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_email_address(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::EmailAddress)
|
||||
}
|
||||
|
||||
fn valid_character_count(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::CharacterCount)
|
||||
}
|
||||
|
||||
fn in_invalid_state(&self, flag: InvalidStateFlags) -> bool {
|
||||
match self.submission_state {
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState { ref flags },
|
||||
}) => flags.contains(flag),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn awaiting_submission(&self) -> bool {
|
||||
matches!(
|
||||
self.submission_state,
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.submission_state, Some(SubmissionState::CanSubmit))
|
||||
}
|
||||
|
||||
// TODO: Escape button calls dismiss
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
@@ -390,9 +261,17 @@ impl Render for FeedbackModal {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.update_submission_state(cx);
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
|
||||
let submit_button_text = if self.awaiting_submission() {
|
||||
let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count);
|
||||
|
||||
let allow_submission =
|
||||
valid_character_count && valid_email_address && !self.pending_submission;
|
||||
|
||||
let submit_button_text = if self.pending_submission {
|
||||
"Submitting..."
|
||||
} else {
|
||||
"Submit"
|
||||
@@ -424,13 +303,18 @@ impl Render for FeedbackModal {
|
||||
"Feedback must be at least {} characters.",
|
||||
FEEDBACK_CHAR_LIMIT.start()
|
||||
)
|
||||
} else if self.character_count > *FEEDBACK_CHAR_LIMIT.end() {
|
||||
format!(
|
||||
"Feedback must be less than {} characters.",
|
||||
FEEDBACK_CHAR_LIMIT.end()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Characters: {}",
|
||||
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
|
||||
)
|
||||
})
|
||||
.color(if self.valid_character_count() {
|
||||
.color(if valid_character_count {
|
||||
Color::Success
|
||||
} else {
|
||||
Color::Error
|
||||
@@ -454,11 +338,7 @@ impl Render for FeedbackModal {
|
||||
.p_2()
|
||||
.border()
|
||||
.rounded_md()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
red()
|
||||
})
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.email_address_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
@@ -491,11 +371,14 @@ impl Render for FeedbackModal {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("submit_feedback", submit_button_text)
|
||||
Button::new("send_feedback", submit_button_text)
|
||||
.color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
// TODO: Ensure that while submitting, "Sending..." is shown and disable the button
|
||||
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button
|
||||
// TODO: If submit is successful, close the modal
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.submit(cx).detach();
|
||||
let _ = this.submit(cx);
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
@@ -505,7 +388,7 @@ impl Render for FeedbackModal {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.when(!self.can_submit(), |this| this.disabled(true)),
|
||||
.when(!allow_submission, |this| this.disabled(true)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -513,44 +396,6 @@ impl Render for FeedbackModal {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add compilation flags to enable debug mode, where we can simulate sending feedback that both succeeds and fails, so we can test the UI
|
||||
// TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
|
||||
// TODO: Testing of various button states, dismissal prompts, etc.
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn test_invalid_email_addresses() {
|
||||
// let markdown = markdown.await.log_err();
|
||||
// let buffer = project.update(&mut cx, |project, cx| {
|
||||
// project.create_buffer("", markdown, cx)
|
||||
// })??;
|
||||
|
||||
// workspace.update(&mut cx, |workspace, cx| {
|
||||
// let system_specs = SystemSpecs::new(cx);
|
||||
|
||||
// workspace.toggle_modal(cx, move |cx| {
|
||||
// let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx);
|
||||
|
||||
// assert!(!feedback_modal.can_submit());
|
||||
// assert!(!feedback_modal.valid_email_address(cx));
|
||||
// assert!(!feedback_modal.valid_character_count());
|
||||
|
||||
// feedback_modal
|
||||
// .email_address_editor
|
||||
// .update(cx, |this, cx| this.set_text("a", cx));
|
||||
// feedback_modal.set_submission_state(cx);
|
||||
|
||||
// assert!(!feedback_modal.valid_email_address(cx));
|
||||
|
||||
// feedback_modal
|
||||
// .email_address_editor
|
||||
// .update(cx, |this, cx| this.set_text("a&b.com", cx));
|
||||
// feedback_modal.set_submission_state(cx);
|
||||
|
||||
// assert!(feedback_modal.valid_email_address(cx));
|
||||
// });
|
||||
// })?;
|
||||
// }
|
||||
// }
|
||||
// TODO: Fix bug of being asked twice to discard feedback when clicking cancel
|
||||
|
||||
@@ -21,7 +21,7 @@ impl SystemSpecs {
|
||||
let app_version = ZED_APP_VERSION
|
||||
.or_else(|| cx.app_metadata().app_version)
|
||||
.map(|v| v.to_string());
|
||||
let release_channel = cx.global::<ReleaseChannel>().display_name();
|
||||
let release_channel = cx.global::<ReleaseChannel>().dev_name();
|
||||
let os_name = cx.app_metadata().os_name;
|
||||
let system = System::new_all();
|
||||
let memory = system.total_memory();
|
||||
|
||||
@@ -19,7 +19,7 @@ use ui::{prelude::*, HighlightedLabel, ListItem};
|
||||
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
actions!(file_finder, [Toggle]);
|
||||
actions!(Toggle);
|
||||
|
||||
impl ModalView for FileFinder {}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use ui::{h_stack, prelude::*, v_stack, Label};
|
||||
use util::paths::FILE_ROW_COLUMN_DELIMITER;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(go_to_line, [Toggle]);
|
||||
actions!(Toggle);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(GoToLine::register).detach();
|
||||
@@ -92,6 +92,7 @@ impl GoToLine {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
// todo!() this isn't working...
|
||||
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
|
||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||
_ => {}
|
||||
|
||||
@@ -22,7 +22,7 @@ Actions are frequently unit structs, for which we have a macro. The above could
|
||||
|
||||
```rust
|
||||
mod menu {
|
||||
actions!(gpui, [MoveUp, MoveDown]);
|
||||
actions!(MoveUp, MoveDown);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,33 +3,34 @@ use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashMap;
|
||||
pub use no_action::NoAction;
|
||||
use serde_json::json;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
/// Actions are used to implement keyboard-driven UI.
|
||||
/// When you declare an action, you can bind keys to the action in the keymap and
|
||||
/// listeners for that action in the element tree.
|
||||
///
|
||||
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
|
||||
/// action for each listed action name in the given namespace.
|
||||
/// action for each listed action name.
|
||||
/// ```rust
|
||||
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
|
||||
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
|
||||
/// ```
|
||||
/// More complex data types can also be actions, providing they implement Clone, PartialEq,
|
||||
/// and serde_derive::Deserialize.
|
||||
/// Use `impl_actions!` to automatically implement the action in the given namespace.
|
||||
/// More complex data types can also be actions. If you annotate your type with the action derive macro
|
||||
/// it will be implemented and registered automatically.
|
||||
/// ```
|
||||
/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
|
||||
/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
|
||||
/// pub struct SelectNext {
|
||||
/// pub replace_newest: bool,
|
||||
/// }
|
||||
/// impl_actions!(editor, [SelectNext]);
|
||||
/// ```
|
||||
///
|
||||
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
|
||||
/// macro, which only generates the code needed to register your action before `main`.
|
||||
///
|
||||
/// ```
|
||||
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
|
||||
/// #[gpui::register_action]
|
||||
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::fmt::Debug)]
|
||||
/// pub struct Paste {
|
||||
/// pub content: SharedString,
|
||||
/// }
|
||||
@@ -37,7 +38,6 @@ use std::any::{Any, TypeId};
|
||||
/// impl gpui::Action for Paste {
|
||||
/// ///...
|
||||
/// }
|
||||
/// register_action!(Paste);
|
||||
/// ```
|
||||
pub trait Action: 'static {
|
||||
fn boxed_clone(&self) -> Box<dyn Action>;
|
||||
@@ -56,7 +56,7 @@ pub trait Action: 'static {
|
||||
impl std::fmt::Debug for dyn Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn Action")
|
||||
.field("name", &self.name())
|
||||
.field("type_name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ impl ActionRegistry {
|
||||
for builder in __GPUI_ACTIONS {
|
||||
let action = builder();
|
||||
//todo(remove)
|
||||
let name: SharedString = action.name.into();
|
||||
let name: SharedString = remove_the_2(action.name).into();
|
||||
self.builders_by_name.insert(name.clone(), action.build);
|
||||
self.names_by_type_id.insert(action.type_id, name.clone());
|
||||
self.all_names.push(name);
|
||||
@@ -139,9 +139,11 @@ impl ActionRegistry {
|
||||
name: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
//todo(remove)
|
||||
let name = remove_the_2(name);
|
||||
let build_action = self
|
||||
.builders_by_name
|
||||
.get(name)
|
||||
.get(name.deref())
|
||||
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
|
||||
(build_action)(params.unwrap_or_else(|| json!({})))
|
||||
.with_context(|| format!("Attempting to build action {}", name))
|
||||
@@ -153,90 +155,36 @@ impl ActionRegistry {
|
||||
}
|
||||
|
||||
/// Defines unit structs that can be used as actions.
|
||||
/// To use more complex data types as actions, use `impl_actions!`
|
||||
/// To use more complex data types as actions, annotate your type with the #[action] macro.
|
||||
#[macro_export]
|
||||
macro_rules! actions {
|
||||
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
||||
$(
|
||||
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
|
||||
#[serde(crate = "gpui::serde")]
|
||||
pub struct $name;
|
||||
() => {};
|
||||
|
||||
gpui::__impl_action!($namespace, $name,
|
||||
fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(Box::new(Self))
|
||||
}
|
||||
);
|
||||
( $name:ident ) => {
|
||||
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
|
||||
#[serde(crate = "gpui::serde")]
|
||||
pub struct $name;
|
||||
};
|
||||
|
||||
gpui::register_action!($name);
|
||||
)*
|
||||
( $name:ident, $($rest:tt)* ) => {
|
||||
actions!($name);
|
||||
actions!($($rest)*);
|
||||
};
|
||||
}
|
||||
|
||||
/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
|
||||
#[macro_export]
|
||||
macro_rules! impl_actions {
|
||||
($namespace:path, [ $($name:ident),* $(,)? ]) => {
|
||||
$(
|
||||
gpui::__impl_action!($namespace, $name,
|
||||
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
|
||||
}
|
||||
);
|
||||
|
||||
gpui::register_action!($name);
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __impl_action {
|
||||
($namespace:path, $name:ident, $build:item) => {
|
||||
impl gpui::Action for $name {
|
||||
fn name(&self) -> &'static str
|
||||
{
|
||||
concat!(
|
||||
stringify!($namespace),
|
||||
"::",
|
||||
stringify!($name),
|
||||
)
|
||||
}
|
||||
|
||||
// todo!() why is this needed in addition to name?
|
||||
fn debug_name() -> &'static str
|
||||
where
|
||||
Self: ::std::marker::Sized
|
||||
{
|
||||
concat!(
|
||||
stringify!($namespace),
|
||||
"::",
|
||||
stringify!($name),
|
||||
)
|
||||
}
|
||||
|
||||
$build
|
||||
|
||||
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
|
||||
action
|
||||
.as_any()
|
||||
.downcast_ref::<Self>()
|
||||
.map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
|
||||
::std::boxed::Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn ::std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
//todo!(remove)
|
||||
pub fn remove_the_2(action_name: &str) -> String {
|
||||
let mut separator_matches = action_name.rmatch_indices("::");
|
||||
separator_matches.next().unwrap();
|
||||
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
|
||||
// todo!() remove the 2 replacement when migration is done
|
||||
action_name[name_start_ix..]
|
||||
.replace("2::", "::")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
mod no_action {
|
||||
use crate as gpui;
|
||||
|
||||
actions!(zed, [NoAction]);
|
||||
actions!(NoAction);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use derive_more::{Deref, DerefMut};
|
||||
pub use entity_map::*;
|
||||
pub use model_context::*;
|
||||
use refineable::Refineable;
|
||||
use smallvec::SmallVec;
|
||||
use smol::future::FutureExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
@@ -17,14 +18,14 @@ use time::UtcOffset;
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
|
||||
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, GlobalElementId,
|
||||
KeyBinding, Keymap, Keystroke, LayoutId, Menu, MouseDownEvent, PathPromptOptions, Pixels,
|
||||
Platform, PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription,
|
||||
SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window,
|
||||
WindowContext, WindowHandle, WindowId,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
|
||||
ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||
TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
|
||||
WindowHandle, WindowId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{FxHashMap, FxHashSet, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::{channel::oneshot, future::LocalBoxFuture, Future};
|
||||
use parking_lot::Mutex;
|
||||
use slotmap::SlotMap;
|
||||
@@ -170,7 +171,6 @@ impl App {
|
||||
pub(crate) type FrameCallback = Box<dyn FnOnce(&mut AppContext)>;
|
||||
type Handler = Box<dyn FnMut(&mut AppContext) -> bool + 'static>;
|
||||
type Listener = Box<dyn FnMut(&dyn Any, &mut AppContext) -> bool + 'static>;
|
||||
type KeystrokeObserver = Box<dyn FnMut(&KeystrokeEvent, &mut WindowContext) + 'static>;
|
||||
type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()> + 'static>;
|
||||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
|
||||
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
|
||||
@@ -189,30 +189,29 @@ pub struct AppContext {
|
||||
flushing_effects: bool,
|
||||
pending_updates: usize,
|
||||
pub(crate) actions: Rc<ActionRegistry>,
|
||||
pub(crate) mouse_state: MouseState,
|
||||
pub(crate) active_drag: Option<AnyDrag>,
|
||||
pub(crate) active_tooltip: Option<AnyTooltip>,
|
||||
pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
|
||||
pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
|
||||
pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
|
||||
pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
pub(crate) svg_renderer: SvgRenderer,
|
||||
asset_source: Arc<dyn AssetSource>,
|
||||
pub(crate) image_cache: ImageCache,
|
||||
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
|
||||
pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
|
||||
pub(crate) entities: EntityMap,
|
||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) keymap: Arc<Mutex<Keymap>>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
HashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
pub(crate) pending_notifications: FxHashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: FxHashSet<TypeId>,
|
||||
pub(crate) pending_notifications: HashSet<EntityId>,
|
||||
pub(crate) pending_global_notifications: HashSet<TypeId>,
|
||||
pub(crate) observers: SubscriberSet<EntityId, Handler>,
|
||||
// TypeId is the type of the event that the listener callback expects
|
||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
@@ -251,29 +250,28 @@ impl AppContext {
|
||||
actions: Rc::new(ActionRegistry::default()),
|
||||
flushing_effects: false,
|
||||
pending_updates: 0,
|
||||
mouse_state: MouseState::None,
|
||||
active_drag: None,
|
||||
active_tooltip: None,
|
||||
next_frame_callbacks: FxHashMap::default(),
|
||||
frame_consumers: FxHashMap::default(),
|
||||
next_frame_callbacks: HashMap::default(),
|
||||
frame_consumers: HashMap::default(),
|
||||
background_executor: executor,
|
||||
foreground_executor,
|
||||
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
||||
asset_source,
|
||||
image_cache: ImageCache::new(http_client),
|
||||
text_style_stack: Vec::new(),
|
||||
globals_by_type: FxHashMap::default(),
|
||||
globals_by_type: HashMap::default(),
|
||||
entities,
|
||||
new_view_observers: SubscriberSet::new(),
|
||||
windows: SlotMap::with_key(),
|
||||
keymap: Arc::new(Mutex::new(Keymap::default())),
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
global_action_listeners: HashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
pending_global_notifications: FxHashSet::default(),
|
||||
pending_notifications: HashSet::default(),
|
||||
pending_global_notifications: HashSet::default(),
|
||||
observers: SubscriberSet::new(),
|
||||
event_listeners: SubscriberSet::new(),
|
||||
release_listeners: SubscriberSet::new(),
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
@@ -516,10 +514,6 @@ impl AppContext {
|
||||
self.platform.path_for_auxiliary_executable(name)
|
||||
}
|
||||
|
||||
pub fn double_click_interval(&self) -> Duration {
|
||||
self.platform.double_click_interval()
|
||||
}
|
||||
|
||||
pub fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
@@ -572,58 +566,53 @@ impl AppContext {
|
||||
loop {
|
||||
self.release_dropped_entities();
|
||||
self.release_dropped_focus_handles();
|
||||
|
||||
if let Some(effect) = self.pending_effects.pop_front() {
|
||||
match effect {
|
||||
Effect::Notify { emitter } => {
|
||||
self.apply_notify_effect(emitter);
|
||||
}
|
||||
|
||||
Effect::Emit {
|
||||
emitter,
|
||||
event_type,
|
||||
event,
|
||||
} => self.apply_emit_effect(emitter, event_type, event),
|
||||
|
||||
Effect::FocusChanged {
|
||||
window_handle,
|
||||
focused,
|
||||
} => {
|
||||
self.apply_focus_changed_effect(window_handle, focused);
|
||||
}
|
||||
Effect::Refresh => {
|
||||
self.apply_refresh_effect();
|
||||
}
|
||||
|
||||
Effect::NotifyGlobalObservers { global_type } => {
|
||||
self.apply_notify_global_observers_effect(global_type);
|
||||
}
|
||||
|
||||
Effect::Defer { callback } => {
|
||||
self.apply_defer_effect(callback);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for window in self.windows.values() {
|
||||
if let Some(window) = window.as_ref() {
|
||||
if window.dirty {
|
||||
window.platform_window.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
for window in self
|
||||
.windows
|
||||
.values()
|
||||
.filter_map(|window| {
|
||||
let window = window.as_ref()?;
|
||||
(window.dirty || window.focus_invalidated).then_some(window.handle)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
self.update_window(window, |_, cx| cx.draw()).unwrap();
|
||||
}
|
||||
|
||||
if self.pending_effects.is_empty() {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dirty_window_ids = self
|
||||
.windows
|
||||
.iter()
|
||||
.filter_map(|(_, window)| {
|
||||
let window = window.as_ref()?;
|
||||
if window.dirty {
|
||||
Some(window.handle.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>();
|
||||
|
||||
for dirty_window_handle in dirty_window_ids {
|
||||
dirty_window_handle.update(self, |_, cx| cx.draw()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly called during `flush_effects` to release any entities whose
|
||||
@@ -647,6 +636,8 @@ impl AppContext {
|
||||
}
|
||||
|
||||
/// Repeatedly called during `flush_effects` to handle a focused handle being dropped.
|
||||
/// For now, we simply blur the window if this happens, but we may want to support invoking
|
||||
/// a window blur handler to restore focus to some logical element.
|
||||
fn release_dropped_focus_handles(&mut self) {
|
||||
for window_handle in self.windows() {
|
||||
window_handle
|
||||
@@ -692,6 +683,51 @@ impl AppContext {
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_focus_changed_effect(
|
||||
&mut self,
|
||||
window_handle: AnyWindowHandle,
|
||||
focused: Option<FocusId>,
|
||||
) {
|
||||
window_handle
|
||||
.update(self, |_, cx| {
|
||||
// The window might change focus multiple times in an effect cycle.
|
||||
// We only honor effects for the most recently focused handle.
|
||||
if cx.window.focus == focused {
|
||||
// if someone calls focus multiple times in one frame with the same handle
|
||||
// the first apply_focus_changed_effect will have taken the last blur already
|
||||
// and run the rest of this, so we can return.
|
||||
let Some(last_blur) = cx.window.last_blur.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let focused = focused
|
||||
.map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap());
|
||||
|
||||
let blurred =
|
||||
last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
|
||||
|
||||
let focus_changed = focused.is_some() || blurred.is_some();
|
||||
let event = FocusEvent { focused, blurred };
|
||||
|
||||
let mut listeners = mem::take(&mut cx.window.rendered_frame.focus_listeners);
|
||||
if focus_changed {
|
||||
for listener in &mut listeners {
|
||||
listener(&event, cx);
|
||||
}
|
||||
}
|
||||
cx.window.rendered_frame.focus_listeners = listeners;
|
||||
|
||||
if focus_changed {
|
||||
cx.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn apply_refresh_effect(&mut self) {
|
||||
for window in self.windows.values_mut() {
|
||||
if let Some(window) = window.as_mut() {
|
||||
@@ -919,15 +955,6 @@ impl AppContext {
|
||||
subscription
|
||||
}
|
||||
|
||||
pub fn observe_keystrokes(
|
||||
&mut self,
|
||||
f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = self.keystroke_observers.insert((), Box::new(f));
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
|
||||
self.text_style_stack.push(text_style);
|
||||
}
|
||||
@@ -1090,24 +1117,7 @@ impl AppContext {
|
||||
}
|
||||
|
||||
pub fn has_active_drag(&self) -> bool {
|
||||
self.mouse_state.is_dragging()
|
||||
}
|
||||
|
||||
pub fn active_drag<T: 'static>(&self) -> Option<&T> {
|
||||
if let MouseState::Dragging(drag) = &self.mouse_state {
|
||||
drag.value.downcast_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element_clicked(&self, element_id: &GlobalElementId) -> bool {
|
||||
if let MouseState::Clicked { clicked_id, .. } = &self.mouse_state {
|
||||
if clicked_id == element_id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
self.active_drag.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,6 +1227,10 @@ pub(crate) enum Effect {
|
||||
event_type: TypeId,
|
||||
event: Box<dyn Any>,
|
||||
},
|
||||
FocusChanged {
|
||||
window_handle: AnyWindowHandle,
|
||||
focused: Option<FocusId>,
|
||||
},
|
||||
Refresh,
|
||||
NotifyGlobalObservers {
|
||||
global_type: TypeId,
|
||||
@@ -1255,69 +1269,10 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum MouseState {
|
||||
#[default]
|
||||
None,
|
||||
Clicked {
|
||||
clicked_id: GlobalElementId,
|
||||
event: MouseDownEvent,
|
||||
},
|
||||
Dragging(AnyDrag),
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
pub fn as_clicked(&self, element_id: &GlobalElementId) -> Option<&MouseDownEvent> {
|
||||
if let MouseState::Clicked {
|
||||
clicked_id: clicked_element_id,
|
||||
event,
|
||||
} = self
|
||||
{
|
||||
if clicked_element_id == element_id {
|
||||
Some(event)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_click(&mut self, element_id: &GlobalElementId) -> Option<MouseDownEvent> {
|
||||
match mem::take(self) {
|
||||
MouseState::None | MouseState::Dragging(_) => None,
|
||||
MouseState::Clicked { clicked_id, event } => {
|
||||
if &clicked_id == element_id {
|
||||
Some(event)
|
||||
} else {
|
||||
*self = MouseState::Clicked { clicked_id, event };
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
matches!(self, Self::Dragging(_))
|
||||
}
|
||||
|
||||
pub fn take_drag(&mut self) -> Option<AnyDrag> {
|
||||
match mem::take(self) {
|
||||
MouseState::None => None,
|
||||
state @ MouseState::Clicked { .. } => {
|
||||
*self = state;
|
||||
None
|
||||
}
|
||||
MouseState::Dragging(drag) => Some(drag),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains state associated with an active drag operation, started by dragging an element
|
||||
/// within the window or by dragging into the app from the underlying platform.
|
||||
pub struct AnyDrag {
|
||||
pub view: AnyView,
|
||||
pub value: Box<dyn Any>,
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
@@ -1326,9 +1281,3 @@ pub(crate) struct AnyTooltip {
|
||||
pub view: AnyView,
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeystrokeEvent {
|
||||
pub keystroke: Keystroke,
|
||||
pub action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
@@ -567,7 +567,12 @@ impl<'a> VisualTestContext<'a> {
|
||||
pub fn window_title(&mut self) -> Option<String> {
|
||||
self.cx
|
||||
.update_window(self.window, |_, cx| {
|
||||
cx.window.platform_window.as_test().unwrap().title.clone()
|
||||
cx.window
|
||||
.platform_window
|
||||
.as_test()
|
||||
.unwrap()
|
||||
.window_title
|
||||
.clone()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
use std::{
|
||||
alloc,
|
||||
cell::Cell,
|
||||
ops::{Deref, DerefMut},
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
struct ArenaElement {
|
||||
value: NonNull<u8>,
|
||||
drop: unsafe fn(NonNull<u8>),
|
||||
}
|
||||
|
||||
impl Drop for ArenaElement {
|
||||
#[inline(always)]
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
(self.drop)(self.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Arena {
|
||||
start: NonNull<u8>,
|
||||
offset: usize,
|
||||
elements: Vec<ArenaElement>,
|
||||
valid: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl Arena {
|
||||
pub fn new(size_in_bytes: usize) -> Self {
|
||||
unsafe {
|
||||
let layout = alloc::Layout::from_size_align(size_in_bytes, 1).unwrap();
|
||||
let ptr = alloc::alloc(layout);
|
||||
Self {
|
||||
start: NonNull::new_unchecked(ptr),
|
||||
offset: 0,
|
||||
elements: Vec::new(),
|
||||
valid: Rc::new(Cell::new(true)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.valid.set(false);
|
||||
self.valid = Rc::new(Cell::new(true));
|
||||
self.elements.clear();
|
||||
self.offset = 0;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn alloc<T>(&mut self, f: impl FnOnce() -> T) -> ArenaRef<T> {
|
||||
#[inline(always)]
|
||||
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
ptr::write(ptr, f());
|
||||
}
|
||||
|
||||
unsafe fn drop<T>(ptr: NonNull<u8>) {
|
||||
std::ptr::drop_in_place(ptr.cast::<T>().as_ptr());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let layout = alloc::Layout::new::<T>().pad_to_align();
|
||||
let ptr = NonNull::new_unchecked(self.start.as_ptr().add(self.offset).cast::<T>());
|
||||
inner_writer(ptr.as_ptr(), f);
|
||||
|
||||
self.elements.push(ArenaElement {
|
||||
value: ptr.cast(),
|
||||
drop: drop::<T>,
|
||||
});
|
||||
self.offset += layout.size();
|
||||
ArenaRef {
|
||||
ptr,
|
||||
valid: self.valid.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Arena {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArenaRef<T: ?Sized> {
|
||||
ptr: NonNull<T>,
|
||||
valid: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Clone for ArenaRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
ptr: self.ptr,
|
||||
valid: self.valid.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> ArenaRef<T> {
|
||||
#[inline(always)]
|
||||
pub fn map<U: ?Sized>(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaRef<U> {
|
||||
ArenaRef {
|
||||
ptr: unsafe { NonNull::new_unchecked(f(&mut *self)) },
|
||||
valid: self.valid,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(&self) {
|
||||
assert!(
|
||||
self.valid.get(),
|
||||
"attempted to dereference an ArenaRef after its Arena was cleared"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for ArenaRef<T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline(always)]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.validate();
|
||||
unsafe { self.ptr.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for ArenaRef<T> {
|
||||
#[inline(always)]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.validate();
|
||||
unsafe { self.ptr.as_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arena() {
|
||||
let mut arena = Arena::new(1024);
|
||||
let a = arena.alloc(|| 1u64);
|
||||
let b = arena.alloc(|| 2u32);
|
||||
let c = arena.alloc(|| 3u16);
|
||||
let d = arena.alloc(|| 4u8);
|
||||
assert_eq!(*a, 1);
|
||||
assert_eq!(*b, 2);
|
||||
assert_eq!(*c, 3);
|
||||
assert_eq!(*d, 4);
|
||||
|
||||
arena.clear();
|
||||
let a = arena.alloc(|| 5u64);
|
||||
let b = arena.alloc(|| 6u32);
|
||||
let c = arena.alloc(|| 7u16);
|
||||
let d = arena.alloc(|| 8u8);
|
||||
assert_eq!(*a, 5);
|
||||
assert_eq!(*b, 6);
|
||||
assert_eq!(*c, 7);
|
||||
assert_eq!(*d, 8);
|
||||
|
||||
// Ensure drop gets called.
|
||||
let dropped = Rc::new(Cell::new(false));
|
||||
struct DropGuard(Rc<Cell<bool>>);
|
||||
impl Drop for DropGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.set(true);
|
||||
}
|
||||
}
|
||||
arena.alloc(|| DropGuard(dropped.clone()));
|
||||
arena.clear();
|
||||
assert!(dropped.get());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ArenaRef, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size,
|
||||
ViewContext, WindowContext, ELEMENT_ARENA,
|
||||
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub(crate) use smallvec::SmallVec;
|
||||
@@ -23,7 +23,7 @@ pub trait IntoElement: Sized {
|
||||
self.into_element().into_any()
|
||||
}
|
||||
|
||||
fn draw_and_update_state<T, R>(
|
||||
fn draw<T, R>(
|
||||
self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<T>,
|
||||
@@ -92,7 +92,7 @@ pub trait Element: 'static + IntoElement {
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State);
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
@@ -150,8 +150,8 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
let mut element = state.rendered_element.take().unwrap();
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
let element = state.rendered_element.take().unwrap();
|
||||
if let Some(element_id) = element.element_id() {
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
@@ -405,7 +405,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyElement(ArenaRef<dyn ElementObject>);
|
||||
pub struct AnyElement(Box<dyn ElementObject>);
|
||||
|
||||
impl AnyElement {
|
||||
pub fn new<E>(element: E) -> Self
|
||||
@@ -413,17 +413,14 @@ impl AnyElement {
|
||||
E: 'static + Element,
|
||||
E::State: Any,
|
||||
{
|
||||
let element = ELEMENT_ARENA
|
||||
.with_borrow_mut(|arena| arena.alloc(|| Some(DrawableElement::new(element))))
|
||||
.map(|element| element as &mut dyn ElementObject);
|
||||
AnyElement(element)
|
||||
AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
|
||||
}
|
||||
|
||||
pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
|
||||
self.0.layout(cx)
|
||||
}
|
||||
|
||||
pub fn paint(&mut self, cx: &mut WindowContext) {
|
||||
pub fn paint(mut self, cx: &mut WindowContext) {
|
||||
self.0.paint(cx)
|
||||
}
|
||||
|
||||
@@ -438,7 +435,7 @@ impl AnyElement {
|
||||
|
||||
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
@@ -446,6 +443,11 @@ impl AnyElement {
|
||||
self.0.draw(origin, available_space, cx)
|
||||
}
|
||||
|
||||
/// Converts this `AnyElement` into a trait object that can be stored and manipulated.
|
||||
pub fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
|
||||
pub fn inner_id(&self) -> Option<ElementId> {
|
||||
self.0.element_id()
|
||||
}
|
||||
@@ -463,8 +465,8 @@ impl Element for AnyElement {
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.paint(cx)
|
||||
fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,43 +480,50 @@ impl IntoElement for AnyElement {
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_any_element(self) -> AnyElement {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The empty element, which renders nothing.
|
||||
pub type Empty = ();
|
||||
// impl<V, E, F> Element for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
|
||||
// {
|
||||
// type State = Option<AnyElement>;
|
||||
|
||||
impl IntoElement for () {
|
||||
type Element = Self;
|
||||
// fn element_id(&self) -> Option<ElementId> {
|
||||
// None
|
||||
// }
|
||||
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// _: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let render = self.take().unwrap();
|
||||
// let mut element = (render)(view_state, cx).into_any();
|
||||
// let layout_id = element.layout(view_state, cx);
|
||||
// (layout_id, Some(element))
|
||||
// }
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
// fn paint(
|
||||
// self,
|
||||
// _bounds: Bounds<Pixels>,
|
||||
// rendered_element: &mut Self::State,
|
||||
// cx: &mut WindowContext,
|
||||
// ) {
|
||||
// rendered_element.take().unwrap().paint(view_state, cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Element for () {
|
||||
type State = ();
|
||||
// impl<V, E, F> RenderOnce for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
|
||||
// {
|
||||
// type Element = Self;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
(cx.request_layout(&crate::Style::default(), None), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::State,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
}
|
||||
}
|
||||
// fn render(self) -> Self::Element {
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -2,15 +2,15 @@ use refineable::Refineable as _;
|
||||
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||
|
||||
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
Canvas {
|
||||
paint_callback: Some(Box::new(callback)),
|
||||
paint_callback: Box::new(callback),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Canvas {
|
||||
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
|
||||
paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ impl IntoElement for Canvas {
|
||||
}
|
||||
|
||||
impl Element for Canvas {
|
||||
type State = Style;
|
||||
type State = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
@@ -37,13 +37,11 @@ impl Element for Canvas {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = cx.request_layout(&style, []);
|
||||
(layout_id, style)
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
|
||||
style.paint(bounds, cx, |cx| {
|
||||
(self.paint_callback.take().unwrap())(&bounds, cx)
|
||||
});
|
||||
fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
|
||||
(self.paint_callback)(bounds, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,12 +81,11 @@ impl Element for Img {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let source = self.source.clone();
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
bounds.size,
|
||||
@@ -95,7 +94,7 @@ impl Element for Img {
|
||||
|style, _scroll_offset, cx| {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
match source {
|
||||
match self.source {
|
||||
ImageSource::Uri(uri) => {
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
if let Some(data) = image_future
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element,
|
||||
IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
|
||||
WindowContext,
|
||||
px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow as _, DispatchPhase, Element,
|
||||
InteractiveBounds, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement,
|
||||
Styled, WindowContext,
|
||||
};
|
||||
use collections::VecDeque;
|
||||
use refineable::Refineable as _;
|
||||
@@ -24,7 +24,7 @@ pub struct List {
|
||||
pub struct ListState(Rc<RefCell<StateInner>>);
|
||||
|
||||
struct StateInner {
|
||||
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||
last_layout_width: Option<Pixels>,
|
||||
render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
|
||||
items: SumTree<ListItem>,
|
||||
logical_scroll_top: Option<ListOffset>,
|
||||
@@ -84,7 +84,7 @@ impl ListState {
|
||||
let mut items = SumTree::new();
|
||||
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
|
||||
Self(Rc::new(RefCell::new(StateInner {
|
||||
last_layout_bounds: None,
|
||||
last_layout_width: None,
|
||||
render_item: Box::new(render_item),
|
||||
items,
|
||||
logical_scroll_top: None,
|
||||
@@ -153,64 +153,6 @@ impl ListState {
|
||||
}
|
||||
state.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
|
||||
pub fn scroll_to_reveal_item(&self, ix: usize) {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
let mut scroll_top = state.logical_scroll_top();
|
||||
let height = state
|
||||
.last_layout_bounds
|
||||
.map_or(px(0.), |bounds| bounds.size.height);
|
||||
|
||||
if ix <= scroll_top.item_ix {
|
||||
scroll_top.item_ix = ix;
|
||||
scroll_top.offset_in_item = px(0.);
|
||||
} else {
|
||||
let mut cursor = state.items.cursor::<ListItemSummary>();
|
||||
cursor.seek(&Count(ix + 1), Bias::Right, &());
|
||||
let bottom = cursor.start().height;
|
||||
let goal_top = px(0.).max(bottom - height);
|
||||
|
||||
cursor.seek(&Height(goal_top), Bias::Left, &());
|
||||
let start_ix = cursor.start().count;
|
||||
let start_item_top = cursor.start().height;
|
||||
|
||||
if start_ix >= scroll_top.item_ix {
|
||||
scroll_top.item_ix = start_ix;
|
||||
scroll_top.offset_in_item = goal_top - start_item_top;
|
||||
}
|
||||
}
|
||||
|
||||
state.logical_scroll_top = Some(scroll_top);
|
||||
}
|
||||
|
||||
/// Get the bounds for the given item in window coordinates.
|
||||
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
||||
let state = &*self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
let scroll_top = state.logical_scroll_top();
|
||||
|
||||
if ix < scroll_top.item_ix {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = state.items.cursor::<(Count, Height)>();
|
||||
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
|
||||
|
||||
let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item;
|
||||
|
||||
cursor.seek_forward(&Count(ix), Bias::Right, &());
|
||||
if let Some(&ListItem::Rendered { height }) = cursor.item() {
|
||||
let &(Count(count), Height(top)) = cursor.start();
|
||||
if count == ix {
|
||||
let top = bounds.top() + top - scroll_top;
|
||||
return Some(Bounds::from_corners(
|
||||
point(bounds.left(), top),
|
||||
point(bounds.right(), top + height),
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
@@ -293,7 +235,7 @@ impl std::fmt::Debug for ListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ListOffset {
|
||||
pub item_ix: usize,
|
||||
pub offset_in_item: Pixels,
|
||||
@@ -316,7 +258,7 @@ impl Element for List {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
_state: &mut Self::State,
|
||||
cx: &mut crate::WindowContext,
|
||||
@@ -324,9 +266,7 @@ impl Element for List {
|
||||
let state = &mut *self.state.0.borrow_mut();
|
||||
|
||||
// If the width of the list has changed, invalidate all cached item heights
|
||||
if state.last_layout_bounds.map_or(true, |last_bounds| {
|
||||
last_bounds.size.width != bounds.size.width
|
||||
}) {
|
||||
if state.last_layout_width != Some(bounds.size.width) {
|
||||
state.items = SumTree::from_iter(
|
||||
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
|
||||
&(),
|
||||
@@ -446,21 +386,25 @@ impl Element for List {
|
||||
// Paint the visible items
|
||||
let mut item_origin = bounds.origin;
|
||||
item_origin.y -= scroll_top.offset_in_item;
|
||||
for item_element in &mut item_elements {
|
||||
for mut item_element in item_elements {
|
||||
let item_height = item_element.measure(available_item_space, cx).height;
|
||||
item_element.draw(item_origin, available_item_space, cx);
|
||||
item_origin.y += item_height;
|
||||
}
|
||||
|
||||
state.items = new_items;
|
||||
state.last_layout_bounds = Some(bounds);
|
||||
state.last_layout_width = Some(bounds.size.width);
|
||||
|
||||
let interactive_bounds = Rc::new(InteractiveBounds {
|
||||
bounds: bounds.intersect(&cx.content_mask().bounds),
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
});
|
||||
|
||||
let list_state = self.state.clone();
|
||||
let height = bounds.size.height;
|
||||
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& bounds.contains(&event.position)
|
||||
&& cx.was_top_layer(&event.position, cx.stacking_order())
|
||||
if interactive_bounds.visibly_contains(&event.position, cx)
|
||||
&& phase == DispatchPhase::Bubble
|
||||
{
|
||||
list_state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Element for Overlay {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
@@ -106,50 +106,46 @@ impl Element for Overlay {
|
||||
size: cx.viewport_size(),
|
||||
};
|
||||
|
||||
if self.fit_mode == OverlayFitMode::SwitchAnchor {
|
||||
let mut anchor_corner = self.anchor_corner;
|
||||
match self.fit_mode {
|
||||
OverlayFitMode::SnapToWindow => {
|
||||
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||
// its horizontal bounds overflow
|
||||
if desired.right() > limits.right() {
|
||||
desired.origin.x -= desired.right() - limits.right();
|
||||
} else if desired.left() < limits.left() {
|
||||
desired.origin.x = limits.origin.x;
|
||||
}
|
||||
|
||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||
let switched = anchor_corner
|
||||
.switch_axis(Axis::Horizontal)
|
||||
.get_bounds(origin, size);
|
||||
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
|
||||
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||
// its vertical bounds overflow.
|
||||
if desired.bottom() > limits.bottom() {
|
||||
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||
} else if desired.top() < limits.top() {
|
||||
desired.origin.y = limits.origin.y;
|
||||
}
|
||||
}
|
||||
OverlayFitMode::SwitchAnchor => {
|
||||
let mut anchor_corner = self.anchor_corner;
|
||||
|
||||
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||
desired = switched
|
||||
}
|
||||
|
||||
if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
|
||||
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
||||
}
|
||||
|
||||
// Update bounds if needed
|
||||
if anchor_corner != self.anchor_corner {
|
||||
desired = anchor_corner.get_bounds(origin, size)
|
||||
}
|
||||
}
|
||||
|
||||
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
|
||||
let switched = anchor_corner
|
||||
.switch_axis(Axis::Vertical)
|
||||
.get_bounds(origin, size);
|
||||
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
|
||||
desired = switched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
|
||||
if desired.right() > limits.right() {
|
||||
desired.origin.x -= desired.right() - limits.right();
|
||||
}
|
||||
if desired.left() < limits.left() {
|
||||
desired.origin.x = limits.origin.x;
|
||||
}
|
||||
|
||||
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
|
||||
if desired.bottom() > limits.bottom() {
|
||||
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||
}
|
||||
if desired.top() < limits.top() {
|
||||
desired.origin.y = limits.origin.y;
|
||||
OverlayFitMode::None => {}
|
||||
}
|
||||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
cx.break_content_mask(|cx| {
|
||||
for child in &mut self.children {
|
||||
for child in self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
@@ -174,10 +170,11 @@ enum Axis {
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum OverlayFitMode {
|
||||
SnapToWindow,
|
||||
SwitchAnchor,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -36,12 +36,8 @@ impl Element for Svg {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) where
|
||||
fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
impl Element for &'static str {
|
||||
@@ -22,7 +22,7 @@ impl Element for &'static str {
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
state.paint(bounds, self, cx)
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ impl Element for SharedString {
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
let text_str: &str = self.as_ref();
|
||||
state.paint(bounds, text_str, cx)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ impl Element for StyledText {
|
||||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state.paint(bounds, &self.text, cx)
|
||||
}
|
||||
}
|
||||
@@ -253,7 +253,7 @@ impl TextState {
|
||||
}
|
||||
|
||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||
if !bounds.contains(&position) {
|
||||
if !bounds.contains_point(&position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -356,8 +356,8 @@ impl Element for InteractiveText {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
if let Some(click_listener) = self.click_listener {
|
||||
if let Some(ix) = state
|
||||
.text_state
|
||||
.index_for_position(bounds, cx.mouse_position())
|
||||
@@ -374,14 +374,13 @@ impl Element for InteractiveText {
|
||||
let text_state = state.text_state.clone();
|
||||
let mouse_down = state.mouse_down_index.clone();
|
||||
if let Some(mouse_down_index) = mouse_down.get() {
|
||||
let clickable_ranges = mem::take(&mut self.clickable_ranges);
|
||||
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
if let Some(mouse_up_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
{
|
||||
click_listener(
|
||||
&clickable_ranges,
|
||||
&self.clickable_ranges,
|
||||
InteractiveTextClickEvent {
|
||||
mouse_down_index,
|
||||
mouse_up_index,
|
||||
|
||||
@@ -10,7 +10,6 @@ use taffy::style::Overflow;
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
#[track_caller]
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: View<V>,
|
||||
id: I,
|
||||
@@ -42,11 +41,7 @@ where
|
||||
render_items: Box::new(render_range),
|
||||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
base_style: Box::new(base_style),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
location: Some(*core::panic::Location::caller()),
|
||||
|
||||
base_style,
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
@@ -155,12 +150,14 @@ impl Element for UniformList {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let style = self.interactivity.compute_style(Some(bounds), cx);
|
||||
let style =
|
||||
self.interactivity
|
||||
.compute_style(Some(bounds), &mut element_state.interactive, cx);
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
@@ -189,27 +186,20 @@ impl Element for UniformList {
|
||||
content_size,
|
||||
&mut element_state.interactive,
|
||||
cx,
|
||||
|style, mut scroll_offset, cx| {
|
||||
|style, scroll_offset, cx| {
|
||||
let border = style.border_widths.to_pixels(cx.rem_size());
|
||||
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
|
||||
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top),
|
||||
bounds.lower_right() - point(border.right + padding.right, border.bottom),
|
||||
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||
bounds.lower_right()
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
|
||||
style.paint(bounds, cx, |cx| {
|
||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
|
||||
style.paint(bounds, cx);
|
||||
|
||||
if self.item_count > 0 {
|
||||
let content_height =
|
||||
item_height * self.item_count + padding.top + padding.bottom;
|
||||
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||
let is_scrolled = scroll_offset.y != px(0.);
|
||||
|
||||
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
}
|
||||
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
@@ -219,23 +209,20 @@ impl Element for UniformList {
|
||||
}
|
||||
|
||||
let first_visible_element_ix =
|
||||
(-(scroll_offset.y + padding.top) / item_height).floor() as usize;
|
||||
(-scroll_offset.y / item_height).floor() as usize;
|
||||
let last_visible_element_ix =
|
||||
((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
|
||||
as usize;
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||
let items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in items.iter_mut().zip(visible_range) {
|
||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(
|
||||
px(0.),
|
||||
item_height * ix + scroll_offset.y + padding.top,
|
||||
);
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
@@ -245,9 +232,9 @@ impl Element for UniformList {
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,8 @@ where
|
||||
T: 'static,
|
||||
E: 'static + Debug,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||
let location = core::panic::Location::caller();
|
||||
cx.foreground_executor()
|
||||
.spawn(self.log_tracked_err(*location))
|
||||
.detach();
|
||||
cx.foreground_executor().spawn(self.log_err()).detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,62 +8,6 @@ use std::{
|
||||
ops::{Add, Div, Mul, MulAssign, Sub},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum Axis {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
impl Axis {
|
||||
pub fn invert(&self) -> Self {
|
||||
match self {
|
||||
Axis::Vertical => Axis::Horizontal,
|
||||
Axis::Horizontal => Axis::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Along {
|
||||
type Unit;
|
||||
|
||||
fn along(&self, axis: Axis) -> Self::Unit;
|
||||
|
||||
fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self;
|
||||
}
|
||||
|
||||
impl sqlez::bindable::StaticColumnCount for Axis {}
|
||||
impl sqlez::bindable::Bind for Axis {
|
||||
fn bind(
|
||||
&self,
|
||||
statement: &sqlez::statement::Statement,
|
||||
start_index: i32,
|
||||
) -> anyhow::Result<i32> {
|
||||
match self {
|
||||
Axis::Horizontal => "Horizontal",
|
||||
Axis::Vertical => "Vertical",
|
||||
}
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl sqlez::bindable::Column for Axis {
|
||||
fn column(
|
||||
statement: &mut sqlez::statement::Statement,
|
||||
start_index: i32,
|
||||
) -> anyhow::Result<(Self, i32)> {
|
||||
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
|
||||
Ok((
|
||||
match axis_text.as_str() {
|
||||
"Horizontal" => Axis::Horizontal,
|
||||
"Vertical" => Axis::Vertical,
|
||||
_ => anyhow::bail!("Stored serialized item kind is incorrect"),
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a location in a 2D cartesian coordinate space.
|
||||
///
|
||||
/// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
|
||||
@@ -152,30 +96,6 @@ impl<T: Clone + Debug + Default> Point<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Debug + Default> Along for Point<T> {
|
||||
type Unit = T;
|
||||
|
||||
fn along(&self, axis: Axis) -> T {
|
||||
match axis {
|
||||
Axis::Horizontal => self.x.clone(),
|
||||
Axis::Vertical => self.y.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Point<T> {
|
||||
match axis {
|
||||
Axis::Horizontal => Point {
|
||||
x: f(self.x.clone()),
|
||||
y: self.y.clone(),
|
||||
},
|
||||
Axis::Vertical => Point {
|
||||
x: self.x.clone(),
|
||||
y: f(self.y.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Point<Pixels> {
|
||||
/// Scales the point by a given factor, which is typically derived from the resolution
|
||||
/// of a target display to ensure proper sizing of UI elements.
|
||||
@@ -453,34 +373,6 @@ impl Size<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Along for Size<T>
|
||||
where
|
||||
T: Clone + Default + Debug,
|
||||
{
|
||||
type Unit = T;
|
||||
|
||||
fn along(&self, axis: Axis) -> T {
|
||||
match axis {
|
||||
Axis::Horizontal => self.width.clone(),
|
||||
Axis::Vertical => self.height.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value of this size along the given axis.
|
||||
fn apply_along(&self, axis: Axis, f: impl FnOnce(T) -> T) -> Self {
|
||||
match axis {
|
||||
Axis::Horizontal => Size {
|
||||
width: f(self.width.clone()),
|
||||
height: self.height.clone(),
|
||||
},
|
||||
Axis::Vertical => Size {
|
||||
width: self.width.clone(),
|
||||
height: f(self.height.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Size<T>
|
||||
where
|
||||
T: PartialOrd + Clone + Default + Debug,
|
||||
@@ -1100,7 +992,7 @@ where
|
||||
/// assert!(bounds.contains_point(&inside_point));
|
||||
/// assert!(!bounds.contains_point(&outside_point));
|
||||
/// ```
|
||||
pub fn contains(&self, point: &Point<T>) -> bool {
|
||||
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
|
||||
@@ -1592,17 +1484,6 @@ impl Edges<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Edges<Pixels>> for f32 {
|
||||
fn into(self) -> Edges<Pixels> {
|
||||
Edges {
|
||||
top: self.into(),
|
||||
right: self.into(),
|
||||
bottom: self.into(),
|
||||
left: self.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the corners of a box in a 2D space, such as border radius.
|
||||
///
|
||||
/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
|
||||
@@ -1819,28 +1700,6 @@ where
|
||||
|
||||
impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
|
||||
|
||||
impl Into<Corners<Pixels>> for f32 {
|
||||
fn into(self) -> Corners<Pixels> {
|
||||
Corners {
|
||||
top_left: self.into(),
|
||||
top_right: self.into(),
|
||||
bottom_right: self.into(),
|
||||
bottom_left: self.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Corners<Pixels>> for Pixels {
|
||||
fn into(self) -> Corners<Pixels> {
|
||||
Corners {
|
||||
top_left: self,
|
||||
top_right: self,
|
||||
bottom_right: self,
|
||||
bottom_left: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a length in pixels, the base unit of measurement in the UI framework.
|
||||
///
|
||||
/// `Pixels` is a value type that represents an absolute length in pixels, which is used
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
mod action;
|
||||
mod app;
|
||||
|
||||
mod arena;
|
||||
mod assets;
|
||||
mod color;
|
||||
mod element;
|
||||
@@ -39,7 +38,6 @@ mod private {
|
||||
pub use action::*;
|
||||
pub use anyhow::Result;
|
||||
pub use app::*;
|
||||
pub(crate) use arena::*;
|
||||
pub use assets::*;
|
||||
pub use color::*;
|
||||
pub use ctor::ctor;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
div, point, Div, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext,
|
||||
div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point,
|
||||
Render, ViewContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
|
||||
@@ -130,12 +131,6 @@ pub struct MouseMoveEvent {
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl MouseMoveEvent {
|
||||
pub fn dragging(&self) -> bool {
|
||||
self.pressed_button == Some(MouseButton::Left)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Point<Pixels>,
|
||||
@@ -289,6 +284,11 @@ impl InputEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FocusEvent {
|
||||
pub blurred: Option<FocusHandle>,
|
||||
pub focused: Option<FocusHandle>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
@@ -302,7 +302,7 @@ mod test {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
actions!(test, [TestAction]);
|
||||
actions!(TestAction);
|
||||
|
||||
impl Render for TestView {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
arena::ArenaRef, Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext,
|
||||
KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
|
||||
Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
|
||||
Keystroke, KeystrokeMatcher, WindowContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -26,19 +26,18 @@ pub(crate) struct DispatchTree {
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DispatchNode {
|
||||
pub key_listeners: Vec<KeyListener>,
|
||||
pub action_listeners: Vec<DispatchActionListener>,
|
||||
pub key_listeners: SmallVec<[KeyListener; 2]>,
|
||||
pub action_listeners: SmallVec<[DispatchActionListener; 16]>,
|
||||
pub context: Option<KeyContext>,
|
||||
focus_id: Option<FocusId>,
|
||||
parent: Option<DispatchNodeId>,
|
||||
}
|
||||
|
||||
type KeyListener = ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
|
||||
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DispatchActionListener {
|
||||
pub(crate) action_type: TypeId,
|
||||
pub(crate) listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl DispatchTree {
|
||||
@@ -117,7 +116,7 @@ impl DispatchTree {
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||
) {
|
||||
self.active_node()
|
||||
.action_listeners
|
||||
@@ -128,9 +127,8 @@ impl DispatchTree {
|
||||
}
|
||||
|
||||
pub fn make_focusable(&mut self, focus_id: FocusId) {
|
||||
let node_id = self.active_node_id();
|
||||
self.active_node().focus_id = Some(focus_id);
|
||||
self.focusable_node_ids.insert(focus_id, node_id);
|
||||
self.focusable_node_ids
|
||||
.insert(focus_id, self.active_node_id());
|
||||
}
|
||||
|
||||
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
|
||||
@@ -210,7 +208,7 @@ impl DispatchTree {
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
context: &[KeyContext],
|
||||
) -> Vec<Box<dyn Action>> {
|
||||
) -> Option<Box<dyn Action>> {
|
||||
if !self.keystroke_matchers.contains_key(context) {
|
||||
let keystroke_contexts = context.iter().cloned().collect();
|
||||
self.keystroke_matchers.insert(
|
||||
@@ -220,24 +218,18 @@ impl DispatchTree {
|
||||
}
|
||||
|
||||
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
|
||||
if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) {
|
||||
if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) {
|
||||
// Clear all pending keystrokes when an action has been found.
|
||||
for keystroke_matcher in self.keystroke_matchers.values_mut() {
|
||||
keystroke_matcher.clear_pending();
|
||||
}
|
||||
|
||||
actions
|
||||
Some(action)
|
||||
} else {
|
||||
vec![]
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
self.keystroke_matchers
|
||||
.iter()
|
||||
.any(|(_, matcher)| matcher.has_pending_keystrokes())
|
||||
}
|
||||
|
||||
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
|
||||
let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
|
||||
let mut current_node_id = Some(target);
|
||||
@@ -249,20 +241,6 @@ impl DispatchTree {
|
||||
dispatch_path
|
||||
}
|
||||
|
||||
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
|
||||
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
|
||||
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
let node = self.node(node_id);
|
||||
if let Some(focus_id) = node.focus_id {
|
||||
focus_path.push(focus_id);
|
||||
}
|
||||
current_node_id = node.parent;
|
||||
}
|
||||
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
focus_path
|
||||
}
|
||||
|
||||
pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
|
||||
&self.nodes[node_id.0]
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ impl KeyBinding {
|
||||
{
|
||||
// If the binding is completed, push it onto the matches list
|
||||
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
||||
KeyMatch::Some(vec![self.action.boxed_clone()])
|
||||
KeyMatch::Some(self.action.boxed_clone())
|
||||
} else {
|
||||
KeyMatch::Pending
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use smallvec::SmallVec;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Default, Eq, PartialEq, Hash)]
|
||||
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
|
||||
pub struct KeyContext(SmallVec<[ContextEntry; 8]>);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
struct ContextEntry {
|
||||
@@ -293,13 +293,11 @@ mod tests {
|
||||
#[test]
|
||||
fn test_actions_definition() {
|
||||
{
|
||||
actions!(test, [A, B, C, D, E, F, G]);
|
||||
actions!(A, B, C, D, E, F, G);
|
||||
}
|
||||
|
||||
{
|
||||
actions!(
|
||||
test,
|
||||
[
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
@@ -307,7 +305,6 @@ mod tests {
|
||||
E,
|
||||
F,
|
||||
G, // Don't wrap, test the trailing comma
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +54,14 @@ impl KeystrokeMatcher {
|
||||
}
|
||||
|
||||
let mut pending_key = None;
|
||||
let mut found_actions = Vec::new();
|
||||
|
||||
for binding in keymap.bindings().iter().rev() {
|
||||
for candidate in keystroke.match_candidates() {
|
||||
self.pending_keystrokes.push(candidate.clone());
|
||||
match binding.match_keystrokes(&self.pending_keystrokes, context_stack) {
|
||||
KeyMatch::Some(mut actions) => {
|
||||
found_actions.append(&mut actions);
|
||||
KeyMatch::Some(action) => {
|
||||
self.pending_keystrokes.clear();
|
||||
return KeyMatch::Some(action);
|
||||
}
|
||||
KeyMatch::Pending => {
|
||||
pending_key.get_or_insert(candidate);
|
||||
@@ -72,11 +72,6 @@ impl KeystrokeMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
if !found_actions.is_empty() {
|
||||
self.pending_keystrokes.clear();
|
||||
return KeyMatch::Some(found_actions);
|
||||
}
|
||||
|
||||
if let Some(pending_key) = pending_key {
|
||||
self.pending_keystrokes.push(pending_key);
|
||||
}
|
||||
@@ -106,7 +101,7 @@ impl KeystrokeMatcher {
|
||||
pub enum KeyMatch {
|
||||
None,
|
||||
Pending,
|
||||
Some(Vec<Box<dyn Action>>),
|
||||
Some(Box<dyn Action>),
|
||||
}
|
||||
|
||||
impl KeyMatch {
|
||||
|
||||
@@ -46,8 +46,6 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
Rc::new(MacPlatform::new())
|
||||
}
|
||||
|
||||
pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
|
||||
|
||||
pub(crate) trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
fn foreground_executor(&self) -> ForegroundExecutor;
|
||||
@@ -68,7 +66,6 @@ pub(crate) trait Platform: 'static {
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
draw: DrawWindow,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
@@ -105,7 +102,6 @@ pub(crate) trait Platform: 'static {
|
||||
fn app_version(&self) -> Result<SemanticVersion>;
|
||||
fn app_path(&self) -> Result<PathBuf>;
|
||||
fn local_timezone(&self) -> UtcOffset;
|
||||
fn double_click_interval(&self) -> Duration;
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
@@ -147,7 +143,6 @@ pub trait PlatformWindow {
|
||||
fn appearance(&self) -> WindowAppearance;
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||
fn mouse_position(&self) -> Point<Pixels>;
|
||||
fn modifiers(&self) -> Modifiers;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
|
||||
fn clear_input_handler(&mut self);
|
||||
@@ -168,7 +163,7 @@ pub trait PlatformWindow {
|
||||
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 invalidate(&self);
|
||||
fn draw(&self, scene: Scene);
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
Point, Size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::FxHashMap;
|
||||
use collections::HashMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use metal::Device;
|
||||
@@ -53,7 +53,7 @@ struct MetalAtlasState {
|
||||
monochrome_textures: Vec<MetalAtlasTexture>,
|
||||
polychrome_textures: Vec<MetalAtlasTexture>,
|
||||
path_textures: Vec<MetalAtlasTexture>,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
tiles_by_key: HashMap<AtlasKey, AtlasTile>,
|
||||
}
|
||||
|
||||
impl PlatformAtlas for MetalAtlas {
|
||||
|
||||
@@ -303,7 +303,6 @@ impl MetalRenderer {
|
||||
|
||||
command_buffer.commit();
|
||||
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
command_buffer.wait_until_completed();
|
||||
drawable.present();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
|
||||
MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformTextSystem, PlatformWindow, Result, Scene, SemanticVersion, VideoTimestamp,
|
||||
WindowOptions,
|
||||
PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use block::ConcreteBlock;
|
||||
@@ -49,7 +48,6 @@ use std::{
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use time::UtcOffset;
|
||||
|
||||
@@ -492,14 +490,8 @@ impl Platform for MacPlatform {
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
draw: Box<dyn FnMut() -> Result<Scene>>,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
Box::new(MacWindow::open(
|
||||
handle,
|
||||
options,
|
||||
draw,
|
||||
self.foreground_executor(),
|
||||
))
|
||||
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
|
||||
}
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
@@ -658,13 +650,6 @@ impl Platform for MacPlatform {
|
||||
"macOS"
|
||||
}
|
||||
|
||||
fn double_click_interval(&self) -> Duration {
|
||||
unsafe {
|
||||
let double_click_interval: f64 = msg_send![class!(NSEvent), doubleClickInterval];
|
||||
Duration::from_secs_f64(double_click_interval)
|
||||
}
|
||||
}
|
||||
|
||||
fn os_version(&self) -> Result<SemanticVersion> {
|
||||
unsafe {
|
||||
let process_info = NSProcessInfo::processInfo(nil);
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
|
||||
use crate::{
|
||||
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, DrawWindow, ExternalPaths,
|
||||
display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
|
||||
FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
|
||||
PromptLevel, Scene, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
|
||||
NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
|
||||
NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard,
|
||||
NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
|
||||
NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{
|
||||
@@ -46,7 +45,6 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const WINDOW_STATE_IVAR: &str = "windowState";
|
||||
|
||||
@@ -320,7 +318,7 @@ struct MacWindowState {
|
||||
executor: ForegroundExecutor,
|
||||
native_window: id,
|
||||
renderer: MetalRenderer,
|
||||
draw: Option<DrawWindow>,
|
||||
scene_to_render: Option<Scene>,
|
||||
kind: WindowKind,
|
||||
event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
|
||||
activate_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
@@ -456,7 +454,6 @@ impl MacWindow {
|
||||
pub fn open(
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
draw: DrawWindow,
|
||||
executor: ForegroundExecutor,
|
||||
) -> Self {
|
||||
unsafe {
|
||||
@@ -548,7 +545,7 @@ impl MacWindow {
|
||||
executor,
|
||||
native_window,
|
||||
renderer: MetalRenderer::new(true),
|
||||
draw: Some(draw),
|
||||
scene_to_render: None,
|
||||
kind: options.kind,
|
||||
event_callback: None,
|
||||
activate_callback: None,
|
||||
@@ -745,26 +742,6 @@ impl PlatformWindow for MacWindow {
|
||||
convert_mouse_position(position, self.content_size().height)
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
unsafe {
|
||||
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
@@ -981,8 +958,9 @@ impl PlatformWindow for MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate(&self) {
|
||||
let this = self.0.lock();
|
||||
fn draw(&self, scene: Scene) {
|
||||
let mut this = self.0.lock();
|
||||
this.scene_to_render = Some(scene);
|
||||
unsafe {
|
||||
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
|
||||
}
|
||||
@@ -1464,11 +1442,8 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
||||
extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let window_state = get_window_state(this);
|
||||
let mut draw = window_state.lock().draw.take().unwrap();
|
||||
let scene = draw().log_err();
|
||||
let mut window_state = window_state.lock();
|
||||
window_state.draw = Some(draw);
|
||||
if let Some(scene) = scene {
|
||||
let mut window_state = window_state.as_ref().lock();
|
||||
if let Some(scene) = window_state.scene_to_render.take() {
|
||||
window_state.renderer.draw(&scene);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
|
||||
Keymap, Platform, PlatformDisplay, PlatformTextSystem, Scene, TestDisplay, TestWindow,
|
||||
WindowOptions,
|
||||
Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
@@ -12,7 +11,6 @@ use std::{
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct TestPlatform {
|
||||
@@ -130,14 +128,13 @@ impl Platform for TestPlatform {
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<crate::AnyWindowHandle> {
|
||||
self.active_window.lock().clone()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
_draw: Box<dyn FnMut() -> Result<Scene>>,
|
||||
) -> Box<dyn crate::PlatformWindow> {
|
||||
*self.active_window.lock() = Some(handle);
|
||||
Box::new(TestWindow::new(
|
||||
@@ -275,8 +272,4 @@ impl Platform for TestPlatform {
|
||||
fn delete_credentials(&self, _url: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn double_click_interval(&self) -> std::time::Duration {
|
||||
Duration::from_millis(500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
|
||||
PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance,
|
||||
WindowBounds, WindowOptions,
|
||||
PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
|
||||
WindowAppearance, WindowBounds, WindowOptions,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -20,9 +20,9 @@ pub(crate) struct TestWindowHandlers {
|
||||
|
||||
pub struct TestWindow {
|
||||
pub(crate) bounds: WindowBounds,
|
||||
current_scene: Mutex<Option<Scene>>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) edited: bool,
|
||||
pub(crate) window_title: Option<String>,
|
||||
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
|
||||
pub(crate) handlers: Arc<Mutex<TestWindowHandlers>>,
|
||||
platform: Weak<TestPlatform>,
|
||||
@@ -37,13 +37,13 @@ impl TestWindow {
|
||||
) -> Self {
|
||||
Self {
|
||||
bounds: options.bounds,
|
||||
current_scene: Default::default(),
|
||||
display,
|
||||
platform,
|
||||
input_handler: None,
|
||||
sprite_atlas: Arc::new(TestAtlas::new()),
|
||||
handlers: Default::default(),
|
||||
title: Default::default(),
|
||||
edited: false,
|
||||
window_title: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,10 +81,6 @@ impl PlatformWindow for TestWindow {
|
||||
Point::default()
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> crate::Modifiers {
|
||||
crate::Modifiers::default()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
@@ -111,11 +107,11 @@ impl PlatformWindow for TestWindow {
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.title = Some(title.to_owned());
|
||||
self.window_title = Some(title.to_owned());
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
self.edited = edited;
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
@@ -170,8 +166,8 @@ impl PlatformWindow for TestWindow {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn invalidate(&self) {
|
||||
// (self.draw.lock())().unwrap();
|
||||
fn draw(&self, scene: crate::Scene) {
|
||||
self.current_scene.lock().replace(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
|
||||
|
||||
@@ -17,7 +17,6 @@ pub type LayerId = u32;
|
||||
pub type DrawOrder = u32;
|
||||
|
||||
pub(crate) struct SceneBuilder {
|
||||
last_order: Option<(StackingOrder, LayerId)>,
|
||||
layers_by_order: BTreeMap<StackingOrder, LayerId>,
|
||||
splitter: BspSplitter<(PrimitiveKind, usize)>,
|
||||
shadows: Vec<Shadow>,
|
||||
@@ -32,7 +31,6 @@ pub(crate) struct SceneBuilder {
|
||||
impl Default for SceneBuilder {
|
||||
fn default() -> Self {
|
||||
SceneBuilder {
|
||||
last_order: None,
|
||||
layers_by_order: BTreeMap::new(),
|
||||
splitter: BspSplitter::new(),
|
||||
shadows: Vec::new(),
|
||||
@@ -54,7 +52,6 @@ impl SceneBuilder {
|
||||
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
|
||||
}
|
||||
self.layers_by_order.clear();
|
||||
self.last_order = None;
|
||||
|
||||
// Add all primitives to the BSP splitter to determine draw order
|
||||
self.splitter.reset();
|
||||
@@ -159,7 +156,14 @@ impl SceneBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
let layer_id = self.layer_id_for_order(order);
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
|
||||
match primitive {
|
||||
Primitive::Shadow(mut shadow) => {
|
||||
shadow.order = layer_id;
|
||||
@@ -192,24 +196,6 @@ impl SceneBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 {
|
||||
if let Some((last_order, last_layer_id)) = self.last_order.as_ref() {
|
||||
if last_order == order {
|
||||
return *last_layer_id;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
self.last_order = Some((order.clone(), layer_id));
|
||||
layer_id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{iter, mem, ops::Range};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
|
||||
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
|
||||
Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
@@ -14,9 +14,6 @@ pub use taffy::style::{
|
||||
Overflow, Position,
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub struct DebugBelow;
|
||||
|
||||
pub type StyleCascade = Cascade<Style>;
|
||||
|
||||
#[derive(Clone, Refineable, Debug)]
|
||||
@@ -110,12 +107,7 @@ pub struct Style {
|
||||
/// The mouse cursor style shown when the mouse pointer is over an element.
|
||||
pub mouse_cursor: Option<CursorStyle>,
|
||||
|
||||
pub z_index: Option<u8>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug: bool,
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_below: bool,
|
||||
pub z_index: Option<u32>,
|
||||
}
|
||||
|
||||
impl Styled for StyleRefinement {
|
||||
@@ -342,22 +334,7 @@ impl Style {
|
||||
}
|
||||
|
||||
/// Paints the background of an element styled with this style.
|
||||
pub fn paint(
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
continuation: impl FnOnce(&mut WindowContext),
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.set_global(DebugBelow)
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug || cx.has_global::<DebugBelow>() {
|
||||
cx.paint_quad(crate::outline(bounds, crate::red()));
|
||||
}
|
||||
|
||||
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
@@ -371,24 +348,15 @@ impl Style {
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.is_some() || self.is_border_visible() {
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.paint_quad(quad(
|
||||
cx.paint_quad(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
background_color.unwrap_or_default(),
|
||||
self.border_widths.to_pixels(rem_size),
|
||||
self.border_color.unwrap_or_default(),
|
||||
));
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
cx.with_z_index(2, |cx| {
|
||||
continuation(cx);
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.remove_global::<DebugBelow>();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_border_visible(&self) -> bool {
|
||||
@@ -436,11 +404,6 @@ impl Default for Style {
|
||||
text: TextStyleRefinement::default(),
|
||||
mouse_cursor: None,
|
||||
z_index: None,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug: false,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_below: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,14 +608,14 @@ mod tests {
|
||||
(
|
||||
1..2,
|
||||
HighlightStyle {
|
||||
color: Some(green()),
|
||||
color: Some(blue()),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
(
|
||||
2..3,
|
||||
HighlightStyle {
|
||||
color: Some(green()),
|
||||
color: Some(blue()),
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub trait Styled: Sized {
|
||||
|
||||
gpui2_macros::style_helpers!();
|
||||
|
||||
fn z_index(mut self, z_index: u8) -> Self {
|
||||
fn z_index(mut self, z_index: u32) -> Self {
|
||||
self.style().z_index = Some(z_index);
|
||||
self
|
||||
}
|
||||
@@ -303,7 +303,7 @@ pub trait Styled: Sized {
|
||||
|
||||
/// Sets the element to allow a flex item to grow to fill any available space.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex-grow)
|
||||
fn flex_grow(mut self) -> Self {
|
||||
fn grow(mut self) -> Self {
|
||||
self.style().flex_grow = Some(1.);
|
||||
self
|
||||
}
|
||||
@@ -633,16 +633,4 @@ pub trait Styled: Sized {
|
||||
.line_height = Some(line_height.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug(mut self) -> Self {
|
||||
self.style().debug = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_below(mut self) -> Self {
|
||||
self.style().debug_below = Some(true);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style,
|
||||
WindowContext,
|
||||
};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::fmt::Debug;
|
||||
use taffy::{
|
||||
@@ -14,10 +14,10 @@ use taffy::{
|
||||
|
||||
pub struct TaffyLayoutEngine {
|
||||
taffy: Taffy,
|
||||
children_to_parents: FxHashMap<LayoutId, LayoutId>,
|
||||
absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
|
||||
computed_layouts: FxHashSet<LayoutId>,
|
||||
nodes_to_measure: FxHashMap<
|
||||
children_to_parents: HashMap<LayoutId, LayoutId>,
|
||||
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
|
||||
computed_layouts: HashSet<LayoutId>,
|
||||
nodes_to_measure: HashMap<
|
||||
LayoutId,
|
||||
Box<
|
||||
dyn FnMut(
|
||||
@@ -36,10 +36,10 @@ impl TaffyLayoutEngine {
|
||||
pub fn new() -> Self {
|
||||
TaffyLayoutEngine {
|
||||
taffy: Taffy::new(),
|
||||
children_to_parents: FxHashMap::default(),
|
||||
absolute_layout_bounds: FxHashMap::default(),
|
||||
computed_layouts: FxHashSet::default(),
|
||||
nodes_to_measure: FxHashMap::default(),
|
||||
children_to_parents: HashMap::default(),
|
||||
absolute_layout_bounds: HashMap::default(),
|
||||
computed_layouts: HashSet::default(),
|
||||
nodes_to_measure: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,12 +477,3 @@ impl From<Pixels> for AvailableSpace {
|
||||
AvailableSpace::Definite(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<Pixels>> for Size<AvailableSpace> {
|
||||
fn from(size: Size<Pixels>) -> Self {
|
||||
Size {
|
||||
width: AvailableSpace::Definite(size.width),
|
||||
height: AvailableSpace::Definite(size.height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
UnderlineStyle,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use collections::FxHashMap;
|
||||
use collections::HashMap;
|
||||
use core::fmt;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
@@ -37,10 +37,10 @@ pub const SUBPIXEL_VARIANTS: u8 = 4;
|
||||
pub struct TextSystem {
|
||||
line_layout_cache: Arc<LineLayoutCache>,
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
font_ids_by_font: RwLock<FxHashMap<Font, FontId>>,
|
||||
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
|
||||
raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
|
||||
wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<LineWrapper>>>,
|
||||
font_ids_by_font: RwLock<HashMap<Font, FontId>>,
|
||||
font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
|
||||
raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
|
||||
wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
|
||||
font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
|
||||
SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
||||
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
|
||||
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
|
||||
WrappedLineLayout,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
@@ -108,13 +109,16 @@ fn paint_line(
|
||||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
wraps.next();
|
||||
if let Some((background_origin, background_color)) = current_background.as_mut() {
|
||||
cx.paint_quad(fill(
|
||||
cx.paint_quad(
|
||||
Bounds {
|
||||
origin: *background_origin,
|
||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
*background_color,
|
||||
));
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
background_origin.x = origin.x;
|
||||
background_origin.y += line_height;
|
||||
}
|
||||
@@ -176,13 +180,16 @@ fn paint_line(
|
||||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = finished_background {
|
||||
cx.paint_quad(fill(
|
||||
cx.paint_quad(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
background_color,
|
||||
));
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||
@@ -228,13 +235,16 @@ fn paint_line(
|
||||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = current_background.take() {
|
||||
cx.paint_quad(fill(
|
||||
cx.paint_quad(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
size: size(last_line_end_x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
background_color,
|
||||
));
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use collections::FxHashMap;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -236,10 +236,10 @@ impl WrappedLineLayout {
|
||||
}
|
||||
|
||||
pub(crate) struct LineLayoutCache {
|
||||
previous_frame: Mutex<FxHashMap<CacheKey, Arc<LineLayout>>>,
|
||||
current_frame: RwLock<FxHashMap<CacheKey, Arc<LineLayout>>>,
|
||||
previous_frame_wrapped: Mutex<FxHashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
current_frame_wrapped: RwLock<FxHashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
|
||||
current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
|
||||
previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
}
|
||||
|
||||
|
||||