Compare commits
4 Commits
v1.0.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ccbcc644db | |||
| 15c5ce7677 | |||
| 40d726c986 | |||
| fe4eb7df74 |
278
Cargo.lock
generated
278
Cargo.lock
generated
@@ -220,9 +220,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.13.3"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e21900ac91937e4d9a51391f3569cd92fc38caea1a2a671d56b39797f3ece61f"
|
||||
checksum = "5df42e54d7015b7a00d2db9f5cb9975bc4b0cd46627e666c94c3534207503e71"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
@@ -1200,7 +1200,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -1316,9 +1316,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coop"
|
||||
@@ -1633,9 +1636,18 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
@@ -1647,7 +1659,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1937,7 +1949,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2001,6 +2013,18 @@ dependencies = [
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -2611,7 +2635,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.2.2"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -2690,7 +2714,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_linux"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as-raw-xcb-connection",
|
||||
@@ -2738,7 +2762,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macos"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-task",
|
||||
@@ -2780,7 +2804,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -2791,7 +2815,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_platform"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"gpui",
|
||||
@@ -2804,7 +2828,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_tokio"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
@@ -2815,7 +2839,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
@@ -2824,7 +2848,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_web"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"console_error_panic_hook",
|
||||
@@ -2848,7 +2872,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_wgpu"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
@@ -2876,7 +2900,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_windows"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
@@ -2937,9 +2961,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c"
|
||||
checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bytemuck",
|
||||
@@ -3119,7 +3143,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -3144,7 +3168,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
@@ -3690,6 +3714,17 @@ dependencies = [
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linebender_resource_handle"
|
||||
version = "0.1.1"
|
||||
@@ -3924,7 +3959,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
@@ -4065,7 +4100,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
@@ -4238,6 +4273,18 @@ dependencies = [
|
||||
"nostr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-gossip-sqlite"
|
||||
version = "0.44.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
"nostr-gossip",
|
||||
"rusqlite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.44.0"
|
||||
@@ -4252,6 +4299,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-memory"
|
||||
version = "0.44.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#9bcc6cd779a7c6eb41509b37aee4575fa5ae47b9"
|
||||
dependencies = [
|
||||
"btreecap",
|
||||
"nostr",
|
||||
"nostr-database",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.44.1"
|
||||
@@ -4287,7 +4345,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4692,7 +4750,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
[[package]]
|
||||
name = "perf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"serde",
|
||||
@@ -4712,6 +4770,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"state",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4962,11 +5021,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.23.10+spec-1.0.0",
|
||||
"toml_edit 0.25.4+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5089,9 +5148,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5148,7 +5207,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5401,7 +5460,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"derive_refineable",
|
||||
]
|
||||
@@ -5500,7 +5559,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5555,7 +5614,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rope"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
@@ -5573,6 +5632,30 @@ version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rsqlite-vfs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
"sqlite-wasm-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.11.0"
|
||||
@@ -5668,7 +5751,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5817,7 +5900,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"backtrace",
|
||||
@@ -6295,6 +6378,18 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-wasm-rs"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"js-sys",
|
||||
"rsqlite-vfs",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -6349,7 +6444,9 @@ dependencies = [
|
||||
"nostr",
|
||||
"nostr-blossom",
|
||||
"nostr-connect",
|
||||
"nostr-gossip-sqlite",
|
||||
"nostr-lmdb",
|
||||
"nostr-memory",
|
||||
"nostr-sdk",
|
||||
"petname",
|
||||
"rustls",
|
||||
@@ -6411,7 +6508,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
[[package]]
|
||||
name = "sum_tree"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
@@ -6649,7 +6746,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6960,6 +7057,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
@@ -6976,12 +7082,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.23.10+spec-1.0.0"
|
||||
version = "0.25.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
|
||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_datetime 1.0.0+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
@@ -7162,13 +7268,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7310,6 +7416,12 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.45.1"
|
||||
@@ -7358,7 +7470,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
[[package]]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
@@ -7397,7 +7509,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "util_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"perf",
|
||||
"quote",
|
||||
@@ -7406,9 +7518,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.21.0"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
@@ -7470,6 +7582,12 @@ dependencies = [
|
||||
"sval_serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -7671,9 +7789,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.12"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
|
||||
checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
@@ -7685,9 +7803,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.12"
|
||||
version = "0.31.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
|
||||
checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"rustix 1.1.4",
|
||||
@@ -7697,9 +7815,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-cursor"
|
||||
version = "0.31.12"
|
||||
version = "0.31.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078"
|
||||
checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091"
|
||||
dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"wayland-client",
|
||||
@@ -7708,9 +7826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.10"
|
||||
version = "0.32.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
|
||||
checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"wayland-backend",
|
||||
@@ -7720,9 +7838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.10"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b"
|
||||
checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"wayland-backend",
|
||||
@@ -7733,9 +7851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.3.10"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
|
||||
checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"wayland-backend",
|
||||
@@ -7746,20 +7864,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.8"
|
||||
version = "0.31.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
|
||||
checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml 0.39.2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.8"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
|
||||
checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
@@ -7848,7 +7966,7 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
[[package]]
|
||||
name = "wgpu"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 2.11.0",
|
||||
@@ -7877,7 +7995,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
@@ -7908,7 +8026,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-apple"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
@@ -7916,7 +8034,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-emscripten"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
@@ -7924,7 +8042,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
@@ -7932,7 +8050,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
@@ -7979,7 +8097,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "28.0.1"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2#e0f83a6cedc5e0b97da1ebe2d638ad103672e0a2"
|
||||
source = "git+https://github.com/zed-industries/wgpu?rev=9459e95113c5bd116b2cc2c87e8424b28059e17c#9459e95113c5bd116b2cc2c87e8424b28059e17c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bytemuck",
|
||||
@@ -8033,7 +8151,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8637,9 +8755,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -9193,7 +9311,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "zlog"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -9210,7 +9328,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
[[package]]
|
||||
name = "ztracing"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -9221,7 +9339,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ztracing_macro"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#152d3eafcaf4655ac65e2a25e65cc5ee0545db3f"
|
||||
source = "git+https://github.com/zed-industries/zed#a1d8c52a1be83379589438f5dc9665e1f8164dac"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
|
||||
@@ -11,7 +11,7 @@ publish = false
|
||||
[workspace.dependencies]
|
||||
# GPUI
|
||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland", "runtime_shaders"] }
|
||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
|
||||
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
||||
@@ -20,8 +20,10 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
# Nostr
|
||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
|
||||
|
||||
|
||||
3
assets/icons/input.svg
Normal file
3
assets/icons/input.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.75 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V15.25C21.25 16.3546 20.3546 17.25 19.25 17.25H11.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 6.75H4.75C3.64543 6.75 2.75 7.64543 2.75 8.75V15.25C2.75 16.3546 3.64543 17.25 4.75 17.25H5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.75 3.75V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
144
assets/themes/aurora.json
Normal file
144
assets/themes/aurora.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "aurora",
|
||||
"name": "Aurora",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fdfcfeff",
|
||||
"surface_background": "#f8f8ffff",
|
||||
"elevated_surface_background": "#f0f1feff",
|
||||
"panel_background": "#fdfcfeff",
|
||||
"overlay": "#211f4300",
|
||||
"title_bar": "#f0f1feff",
|
||||
"title_bar_inactive": "#fdfcfeff",
|
||||
"window_border": "#dadcffff",
|
||||
"border": "#dadcffff",
|
||||
"border_variant": "#cbcdffff",
|
||||
"border_focused": "#5b5bd6ff",
|
||||
"border_selected": "#5b5bd6ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e6e7ffff",
|
||||
"ring": "#5151cdff",
|
||||
"text": "#1f2d5cff",
|
||||
"text_muted": "#5753c6ff",
|
||||
"text_placeholder": "#9b9ef0ff",
|
||||
"text_accent": "#5b5bd6ff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#5753c6ff",
|
||||
"icon_muted": "#9b9ef0ff",
|
||||
"icon_accent": "#5151cdff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#5b5bd6ff",
|
||||
"element_hover": "#5151cdff",
|
||||
"element_active": "#6e56cfff",
|
||||
"element_selected": "#654dc4ff",
|
||||
"element_disabled": "#5b5bd64d",
|
||||
"secondary_foreground": "#1f2d5cff",
|
||||
"secondary_background": "#f0f1feff",
|
||||
"secondary_hover": "#e6e7ffff",
|
||||
"secondary_active": "#dadcffff",
|
||||
"secondary_selected": "#dadcffff",
|
||||
"secondary_disabled": "#5b5bd64d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f0f1feff",
|
||||
"ghost_element_hover": "#211f430d",
|
||||
"ghost_element_active": "#211f431a",
|
||||
"ghost_element_selected": "#211f431a",
|
||||
"ghost_element_disabled": "#211f4305",
|
||||
"tab_background": "#f0f1feff",
|
||||
"tab_foreground": "#5753c6ff",
|
||||
"tab_hover_background": "#211f430d",
|
||||
"tab_active_background": "#fdfcfeff",
|
||||
"tab_active_foreground": "#1f2d5cff",
|
||||
"scrollbar_thumb_background": "#211f431a",
|
||||
"scrollbar_thumb_hover_background": "#211f4326",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#5b5bd61a",
|
||||
"cursor": "#5b5bd6ff",
|
||||
"selection": "#5b5bd640"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#14121fff",
|
||||
"surface_background": "#1b1525ff",
|
||||
"elevated_surface_background": "#291f43ff",
|
||||
"panel_background": "#14121fff",
|
||||
"overlay": "#baa7ff1a",
|
||||
"title_bar": "#291f43ff",
|
||||
"title_bar_inactive": "#14121fff",
|
||||
"window_border": "#473876ff",
|
||||
"border": "#473876ff",
|
||||
"border_variant": "#3c2e69ff",
|
||||
"border_focused": "#7d66d9ff",
|
||||
"border_selected": "#7d66d9ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#33255bff",
|
||||
"ring": "#6e56cfff",
|
||||
"text": "#e2ddfeff",
|
||||
"text_muted": "#baa7ffff",
|
||||
"text_placeholder": "#6958adff",
|
||||
"text_accent": "#baa7ffff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#baa7ffff",
|
||||
"icon_muted": "#6958adff",
|
||||
"icon_accent": "#6e56cfff",
|
||||
"element_foreground": "#14121fff",
|
||||
"element_background": "#7d66d9ff",
|
||||
"element_hover": "#baa7ffff",
|
||||
"element_active": "#6e56cfff",
|
||||
"element_selected": "#654dc4ff",
|
||||
"element_disabled": "#7d66d94d",
|
||||
"secondary_foreground": "#e2ddfeff",
|
||||
"secondary_background": "#291f43ff",
|
||||
"secondary_hover": "#33255bff",
|
||||
"secondary_active": "#3c2e69ff",
|
||||
"secondary_selected": "#3c2e69ff",
|
||||
"secondary_disabled": "#7d66d94d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#291f43ff",
|
||||
"ghost_element_hover": "#baa7ff0d",
|
||||
"ghost_element_active": "#baa7ff1a",
|
||||
"ghost_element_selected": "#baa7ff1a",
|
||||
"ghost_element_disabled": "#baa7ff05",
|
||||
"tab_background": "#291f43ff",
|
||||
"tab_foreground": "#baa7ffff",
|
||||
"tab_hover_background": "#baa7ff0d",
|
||||
"tab_active_background": "#14121fff",
|
||||
"tab_active_foreground": "#e2ddfeff",
|
||||
"scrollbar_thumb_background": "#baa7ff1a",
|
||||
"scrollbar_thumb_hover_background": "#baa7ff26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#baa7ff1a",
|
||||
"cursor": "#baa7ffff",
|
||||
"selection": "#baa7ff40"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-frappe",
|
||||
"name": "Catppuccin Frappé",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#303446",
|
||||
"surface_background": "#292c3c",
|
||||
"elevated_surface_background": "#232634",
|
||||
"panel_background": "#303446",
|
||||
"overlay": "#c6d0f51a",
|
||||
"title_bar": "#292c3c",
|
||||
"title_bar_inactive": "#232634",
|
||||
"window_border": "#737994",
|
||||
"border": "#626880",
|
||||
"border_variant": "#51576d",
|
||||
"title_bar": "#232634",
|
||||
"title_bar_inactive": "#303446",
|
||||
"window_border": "#51576d",
|
||||
"border": "#51576d",
|
||||
"border_variant": "#414559",
|
||||
"border_focused": "#8caaee",
|
||||
"border_selected": "#8caaee",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#414559",
|
||||
"ring": "#8caaee",
|
||||
"border_transparent": "#c6d0f500",
|
||||
"border_disabled": "#292c3c",
|
||||
"ring": "#babbf1",
|
||||
"text": "#c6d0f5",
|
||||
"text_muted": "#b5bfe2",
|
||||
"text_placeholder": "#a5adce",
|
||||
"text_muted": "#a5adce",
|
||||
"text_placeholder": "#838ba7",
|
||||
"text_accent": "#8caaee",
|
||||
"icon": "#b5bfe2",
|
||||
"icon_muted": "#a5adce",
|
||||
"icon_accent": "#8caaee",
|
||||
"element_foreground": "#232634",
|
||||
"text_danger": "#e78284",
|
||||
"text_warning": "#ef9f76",
|
||||
"icon": "#a5adce",
|
||||
"icon_muted": "#838ba7",
|
||||
"icon_accent": "#babbf1",
|
||||
"element_foreground": "#303446",
|
||||
"element_background": "#8caaee",
|
||||
"element_hover": "#babbf1",
|
||||
"element_active": "#7e99d6",
|
||||
"element_selected": "#7088bf",
|
||||
"element_active": "#99d1db",
|
||||
"element_selected": "#85c1dc",
|
||||
"element_disabled": "#8caaee4d",
|
||||
"secondary_foreground": "#7088bf",
|
||||
"secondary_background": "#292c3c",
|
||||
"secondary_hover": "#8caaee33",
|
||||
"secondary_active": "#232634",
|
||||
"secondary_selected": "#232634",
|
||||
"secondary_foreground": "#c6d0f5",
|
||||
"secondary_background": "#414559",
|
||||
"secondary_hover": "#51576d",
|
||||
"secondary_active": "#626880",
|
||||
"secondary_selected": "#626880",
|
||||
"secondary_disabled": "#8caaee4d",
|
||||
"danger_foreground": "#232634",
|
||||
"danger_foreground": "#303446",
|
||||
"danger_background": "#e78284",
|
||||
"danger_hover": "#ea999c",
|
||||
"danger_active": "#d07576",
|
||||
"danger_selected": "#b96869",
|
||||
"danger_active": "#ef9f76",
|
||||
"danger_selected": "#e5c890",
|
||||
"danger_disabled": "#e782844d",
|
||||
"warning_foreground": "#232634",
|
||||
"warning_background": "#e5c890",
|
||||
"warning_hover": "#ef9f76",
|
||||
"warning_active": "#ceb482",
|
||||
"warning_selected": "#b7a074",
|
||||
"warning_disabled": "#e5c8904d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#414559",
|
||||
"ghost_element_hover": "#c6d0f533",
|
||||
"ghost_element_active": "#51576d",
|
||||
"ghost_element_selected": "#51576d",
|
||||
"ghost_element_disabled": "#c6d0f50d",
|
||||
"tab_inactive_background": "#292c3c",
|
||||
"tab_inactive_foreground": "#b5bfe2",
|
||||
"warning_foreground": "#303446",
|
||||
"warning_background": "#ef9f76",
|
||||
"warning_hover": "#e5c890",
|
||||
"warning_active": "#a6d189",
|
||||
"warning_selected": "#81c8be",
|
||||
"warning_disabled": "#ef9f764d",
|
||||
"ghost_element_background": "#c6d0f500",
|
||||
"ghost_element_background_alt": "#292c3c",
|
||||
"ghost_element_hover": "#c6d0f50d",
|
||||
"ghost_element_active": "#c6d0f51a",
|
||||
"ghost_element_selected": "#c6d0f51a",
|
||||
"ghost_element_disabled": "#c6d0f505",
|
||||
"tab_background": "#232634",
|
||||
"tab_foreground": "#a5adce",
|
||||
"tab_hover_background": "#c6d0f50d",
|
||||
"tab_active_background": "#303446",
|
||||
"tab_active_foreground": "#c6d0f5",
|
||||
"tab_hover_foreground": "#babbf1",
|
||||
"scrollbar_thumb_background": "#c6d0f533",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#51576d",
|
||||
"scrollbar_thumb_background": "#c6d0f51a",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||
"scrollbar_thumb_border": "#c6d0f500",
|
||||
"scrollbar_track_background": "#c6d0f500",
|
||||
"scrollbar_track_border": "#c6d0f500",
|
||||
"drop_target_background": "#8caaee1a",
|
||||
"cursor": "#f2d5cf",
|
||||
"selection": "#949cbb40"
|
||||
"cursor": "#8caaee",
|
||||
"selection": "#8caaee40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#303446",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#232634",
|
||||
"panel_background": "#303446",
|
||||
"overlay": "#c6d0f51a",
|
||||
"title_bar": "#292c3c",
|
||||
"title_bar_inactive": "#232634",
|
||||
"window_border": "#737994",
|
||||
"border": "#626880",
|
||||
"border_variant": "#51576d",
|
||||
"title_bar": "#232634",
|
||||
"title_bar_inactive": "#303446",
|
||||
"window_border": "#51576d",
|
||||
"border": "#51576d",
|
||||
"border_variant": "#414559",
|
||||
"border_focused": "#8caaee",
|
||||
"border_selected": "#8caaee",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#414559",
|
||||
"ring": "#8caaee",
|
||||
"border_transparent": "#c6d0f500",
|
||||
"border_disabled": "#292c3c",
|
||||
"ring": "#babbf1",
|
||||
"text": "#c6d0f5",
|
||||
"text_muted": "#b5bfe2",
|
||||
"text_placeholder": "#a5adce",
|
||||
"text_muted": "#a5adce",
|
||||
"text_placeholder": "#838ba7",
|
||||
"text_accent": "#8caaee",
|
||||
"icon": "#b5bfe2",
|
||||
"icon_muted": "#a5adce",
|
||||
"icon_accent": "#8caaee",
|
||||
"element_foreground": "#232634",
|
||||
"text_danger": "#e78284",
|
||||
"text_warning": "#ef9f76",
|
||||
"icon": "#a5adce",
|
||||
"icon_muted": "#838ba7",
|
||||
"icon_accent": "#babbf1",
|
||||
"element_foreground": "#303446",
|
||||
"element_background": "#8caaee",
|
||||
"element_hover": "#babbf1",
|
||||
"element_active": "#7e99d6",
|
||||
"element_selected": "#7088bf",
|
||||
"element_active": "#99d1db",
|
||||
"element_selected": "#85c1dc",
|
||||
"element_disabled": "#8caaee4d",
|
||||
"secondary_foreground": "#7088bf",
|
||||
"secondary_background": "#292c3c",
|
||||
"secondary_hover": "#8caaee33",
|
||||
"secondary_active": "#232634",
|
||||
"secondary_selected": "#232634",
|
||||
"secondary_foreground": "#c6d0f5",
|
||||
"secondary_background": "#414559",
|
||||
"secondary_hover": "#51576d",
|
||||
"secondary_active": "#626880",
|
||||
"secondary_selected": "#626880",
|
||||
"secondary_disabled": "#8caaee4d",
|
||||
"danger_foreground": "#232634",
|
||||
"danger_foreground": "#303446",
|
||||
"danger_background": "#e78284",
|
||||
"danger_hover": "#ea999c",
|
||||
"danger_active": "#d07576",
|
||||
"danger_selected": "#b96869",
|
||||
"danger_active": "#ef9f76",
|
||||
"danger_selected": "#e5c890",
|
||||
"danger_disabled": "#e782844d",
|
||||
"warning_foreground": "#232634",
|
||||
"warning_background": "#e5c890",
|
||||
"warning_hover": "#ef9f76",
|
||||
"warning_active": "#ceb482",
|
||||
"warning_selected": "#b7a074",
|
||||
"warning_disabled": "#e5c8904d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#414559",
|
||||
"ghost_element_hover": "#c6d0f533",
|
||||
"ghost_element_active": "#51576d",
|
||||
"ghost_element_selected": "#51576d",
|
||||
"ghost_element_disabled": "#c6d0f50d",
|
||||
"tab_inactive_background": "#292c3c",
|
||||
"tab_inactive_foreground": "#b5bfe2",
|
||||
"warning_foreground": "#303446",
|
||||
"warning_background": "#ef9f76",
|
||||
"warning_hover": "#e5c890",
|
||||
"warning_active": "#a6d189",
|
||||
"warning_selected": "#81c8be",
|
||||
"warning_disabled": "#ef9f764d",
|
||||
"ghost_element_background": "#c6d0f500",
|
||||
"ghost_element_background_alt": "#292c3c",
|
||||
"ghost_element_hover": "#c6d0f50d",
|
||||
"ghost_element_active": "#c6d0f51a",
|
||||
"ghost_element_selected": "#c6d0f51a",
|
||||
"ghost_element_disabled": "#c6d0f505",
|
||||
"tab_background": "#232634",
|
||||
"tab_foreground": "#a5adce",
|
||||
"tab_hover_background": "#c6d0f50d",
|
||||
"tab_active_background": "#303446",
|
||||
"tab_active_foreground": "#c6d0f5",
|
||||
"tab_hover_foreground": "#babbf1",
|
||||
"scrollbar_thumb_background": "#c6d0f533",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#51576d",
|
||||
"scrollbar_thumb_background": "#c6d0f51a",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||
"scrollbar_thumb_border": "#c6d0f500",
|
||||
"scrollbar_track_background": "#c6d0f500",
|
||||
"scrollbar_track_border": "#c6d0f500",
|
||||
"drop_target_background": "#8caaee1a",
|
||||
"cursor": "#f2d5cf",
|
||||
"selection": "#949cbb40"
|
||||
"cursor": "#8caaee",
|
||||
"selection": "#8caaee40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-latte",
|
||||
"name": "Catppuccin Latte",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#eff1f5",
|
||||
"surface_background": "#e6e9ef",
|
||||
"elevated_surface_background": "#dce0e8",
|
||||
"panel_background": "#eff1f5",
|
||||
"overlay": "#4c4f691a",
|
||||
"title_bar": "#e6e9ef",
|
||||
"title_bar_inactive": "#dce0e8",
|
||||
"window_border": "#9ca0b0",
|
||||
"border": "#acb0be",
|
||||
"border_variant": "#bcc0cc",
|
||||
"title_bar": "#dce0e8",
|
||||
"title_bar_inactive": "#eff1f5",
|
||||
"window_border": "#bcc0cc",
|
||||
"border": "#bcc0cc",
|
||||
"border_variant": "#ccd0da",
|
||||
"border_focused": "#1e66f5",
|
||||
"border_selected": "#1e66f5",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#ccd0da",
|
||||
"ring": "#1e66f5",
|
||||
"border_transparent": "#4c4f6900",
|
||||
"border_disabled": "#e6e9ef",
|
||||
"ring": "#7287fd",
|
||||
"text": "#4c4f69",
|
||||
"text_muted": "#5c5f77",
|
||||
"text_placeholder": "#6c6f85",
|
||||
"text_muted": "#6c6f85",
|
||||
"text_placeholder": "#8c8fa1",
|
||||
"text_accent": "#1e66f5",
|
||||
"icon": "#5c5f77",
|
||||
"icon_muted": "#6c6f85",
|
||||
"icon_accent": "#1e66f5",
|
||||
"text_danger": "#d20f39",
|
||||
"text_warning": "#fe640b",
|
||||
"icon": "#6c6f85",
|
||||
"icon_muted": "#8c8fa1",
|
||||
"icon_accent": "#7287fd",
|
||||
"element_foreground": "#eff1f5",
|
||||
"element_background": "#1e66f5",
|
||||
"element_hover": "#8839ef",
|
||||
"element_active": "#1c5ce0",
|
||||
"element_selected": "#1a52cc",
|
||||
"element_hover": "#7287fd",
|
||||
"element_active": "#04a5e5",
|
||||
"element_selected": "#209fb5",
|
||||
"element_disabled": "#1e66f54d",
|
||||
"secondary_foreground": "#1a52cc",
|
||||
"secondary_background": "#e6e9ef",
|
||||
"secondary_hover": "#8839ef33",
|
||||
"secondary_active": "#dce0e8",
|
||||
"secondary_selected": "#dce0e8",
|
||||
"secondary_foreground": "#4c4f69",
|
||||
"secondary_background": "#ccd0da",
|
||||
"secondary_hover": "#bcc0cc",
|
||||
"secondary_active": "#acb0be",
|
||||
"secondary_selected": "#acb0be",
|
||||
"secondary_disabled": "#1e66f54d",
|
||||
"danger_foreground": "#eff1f5",
|
||||
"danger_background": "#d20f39",
|
||||
"danger_hover": "#e64553",
|
||||
"danger_active": "#bd0d33",
|
||||
"danger_selected": "#a80b2d",
|
||||
"danger_active": "#fe640b",
|
||||
"danger_selected": "#df8e1d",
|
||||
"danger_disabled": "#d20f394d",
|
||||
"warning_foreground": "#4c4f69",
|
||||
"warning_background": "#df8e1d",
|
||||
"warning_hover": "#fe640b",
|
||||
"warning_active": "#c9801a",
|
||||
"warning_selected": "#b47217",
|
||||
"warning_disabled": "#df8e1d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#ccd0da",
|
||||
"ghost_element_hover": "#4c4f6933",
|
||||
"ghost_element_active": "#bcc0cc",
|
||||
"ghost_element_selected": "#bcc0cc",
|
||||
"ghost_element_disabled": "#4c4f690d",
|
||||
"tab_inactive_background": "#e6e9ef",
|
||||
"tab_inactive_foreground": "#5c5f77",
|
||||
"warning_foreground": "#eff1f5",
|
||||
"warning_background": "#fe640b",
|
||||
"warning_hover": "#df8e1d",
|
||||
"warning_active": "#40a02b",
|
||||
"warning_selected": "#179299",
|
||||
"warning_disabled": "#fe640b4d",
|
||||
"ghost_element_background": "#4c4f6900",
|
||||
"ghost_element_background_alt": "#e6e9ef",
|
||||
"ghost_element_hover": "#4c4f690d",
|
||||
"ghost_element_active": "#4c4f691a",
|
||||
"ghost_element_selected": "#4c4f691a",
|
||||
"ghost_element_disabled": "#4c4f6905",
|
||||
"tab_background": "#e6e9ef",
|
||||
"tab_foreground": "#6c6f85",
|
||||
"tab_hover_background": "#4c4f690d",
|
||||
"tab_active_background": "#eff1f5",
|
||||
"tab_active_foreground": "#4c4f69",
|
||||
"tab_hover_foreground": "#8839ef",
|
||||
"scrollbar_thumb_background": "#4c4f6933",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#bcc0cc",
|
||||
"scrollbar_thumb_background": "#4c4f691a",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||
"scrollbar_thumb_border": "#4c4f6900",
|
||||
"scrollbar_track_background": "#4c4f6900",
|
||||
"scrollbar_track_border": "#4c4f6900",
|
||||
"drop_target_background": "#1e66f51a",
|
||||
"cursor": "#dc8a78",
|
||||
"selection": "#7c7f9340"
|
||||
"cursor": "#1e66f5",
|
||||
"selection": "#1e66f540"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#eff1f5",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#dce0e8",
|
||||
"panel_background": "#eff1f5",
|
||||
"overlay": "#4c4f691a",
|
||||
"title_bar": "#e6e9ef",
|
||||
"title_bar_inactive": "#dce0e8",
|
||||
"window_border": "#9ca0b0",
|
||||
"border": "#acb0be",
|
||||
"border_variant": "#bcc0cc",
|
||||
"title_bar": "#dce0e8",
|
||||
"title_bar_inactive": "#eff1f5",
|
||||
"window_border": "#bcc0cc",
|
||||
"border": "#bcc0cc",
|
||||
"border_variant": "#ccd0da",
|
||||
"border_focused": "#1e66f5",
|
||||
"border_selected": "#1e66f5",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#ccd0da",
|
||||
"ring": "#1e66f5",
|
||||
"border_transparent": "#4c4f6900",
|
||||
"border_disabled": "#e6e9ef",
|
||||
"ring": "#7287fd",
|
||||
"text": "#4c4f69",
|
||||
"text_muted": "#5c5f77",
|
||||
"text_placeholder": "#6c6f85",
|
||||
"text_muted": "#6c6f85",
|
||||
"text_placeholder": "#8c8fa1",
|
||||
"text_accent": "#1e66f5",
|
||||
"icon": "#5c5f77",
|
||||
"icon_muted": "#6c6f85",
|
||||
"icon_accent": "#1e66f5",
|
||||
"text_danger": "#d20f39",
|
||||
"text_warning": "#fe640b",
|
||||
"icon": "#6c6f85",
|
||||
"icon_muted": "#8c8fa1",
|
||||
"icon_accent": "#7287fd",
|
||||
"element_foreground": "#eff1f5",
|
||||
"element_background": "#1e66f5",
|
||||
"element_hover": "#8839ef",
|
||||
"element_active": "#1c5ce0",
|
||||
"element_selected": "#1a52cc",
|
||||
"element_hover": "#7287fd",
|
||||
"element_active": "#04a5e5",
|
||||
"element_selected": "#209fb5",
|
||||
"element_disabled": "#1e66f54d",
|
||||
"secondary_foreground": "#1a52cc",
|
||||
"secondary_background": "#e6e9ef",
|
||||
"secondary_hover": "#8839ef33",
|
||||
"secondary_active": "#dce0e8",
|
||||
"secondary_selected": "#dce0e8",
|
||||
"secondary_foreground": "#4c4f69",
|
||||
"secondary_background": "#ccd0da",
|
||||
"secondary_hover": "#bcc0cc",
|
||||
"secondary_active": "#acb0be",
|
||||
"secondary_selected": "#acb0be",
|
||||
"secondary_disabled": "#1e66f54d",
|
||||
"danger_foreground": "#eff1f5",
|
||||
"danger_background": "#d20f39",
|
||||
"danger_hover": "#e64553",
|
||||
"danger_active": "#bd0d33",
|
||||
"danger_selected": "#a80b2d",
|
||||
"danger_active": "#fe640b",
|
||||
"danger_selected": "#df8e1d",
|
||||
"danger_disabled": "#d20f394d",
|
||||
"warning_foreground": "#4c4f69",
|
||||
"warning_background": "#df8e1d",
|
||||
"warning_hover": "#fe640b",
|
||||
"warning_active": "#c9801a",
|
||||
"warning_selected": "#b47217",
|
||||
"warning_disabled": "#df8e1d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#ccd0da",
|
||||
"ghost_element_hover": "#4c4f6933",
|
||||
"ghost_element_active": "#bcc0cc",
|
||||
"ghost_element_selected": "#bcc0cc",
|
||||
"ghost_element_disabled": "#4c4f690d",
|
||||
"tab_inactive_background": "#e6e9ef",
|
||||
"tab_inactive_foreground": "#5c5f77",
|
||||
"warning_foreground": "#eff1f5",
|
||||
"warning_background": "#fe640b",
|
||||
"warning_hover": "#df8e1d",
|
||||
"warning_active": "#40a02b",
|
||||
"warning_selected": "#179299",
|
||||
"warning_disabled": "#fe640b4d",
|
||||
"ghost_element_background": "#4c4f6900",
|
||||
"ghost_element_background_alt": "#e6e9ef",
|
||||
"ghost_element_hover": "#4c4f690d",
|
||||
"ghost_element_active": "#4c4f691a",
|
||||
"ghost_element_selected": "#4c4f691a",
|
||||
"ghost_element_disabled": "#4c4f6905",
|
||||
"tab_background": "#e6e9ef",
|
||||
"tab_foreground": "#6c6f85",
|
||||
"tab_hover_background": "#4c4f690d",
|
||||
"tab_active_background": "#eff1f5",
|
||||
"tab_active_foreground": "#4c4f69",
|
||||
"tab_hover_foreground": "#8839ef",
|
||||
"scrollbar_thumb_background": "#4c4f6933",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#bcc0cc",
|
||||
"scrollbar_thumb_background": "#4c4f691a",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||
"scrollbar_thumb_border": "#4c4f6900",
|
||||
"scrollbar_track_background": "#4c4f6900",
|
||||
"scrollbar_track_border": "#4c4f6900",
|
||||
"drop_target_background": "#1e66f51a",
|
||||
"cursor": "#dc8a78",
|
||||
"selection": "#7c7f9340"
|
||||
"cursor": "#1e66f5",
|
||||
"selection": "#1e66f540"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-macchiato",
|
||||
"name": "Catppuccin Macchiato",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#24273a",
|
||||
"surface_background": "#1e2030",
|
||||
"elevated_surface_background": "#181926",
|
||||
"panel_background": "#24273a",
|
||||
"overlay": "#cad3f51a",
|
||||
"title_bar": "#1e2030",
|
||||
"title_bar_inactive": "#181926",
|
||||
"window_border": "#6e738d",
|
||||
"border": "#5b6078",
|
||||
"border_variant": "#494d64",
|
||||
"title_bar": "#181926",
|
||||
"title_bar_inactive": "#24273a",
|
||||
"window_border": "#494d64",
|
||||
"border": "#494d64",
|
||||
"border_variant": "#363a4f",
|
||||
"border_focused": "#8aadf4",
|
||||
"border_selected": "#8aadf4",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#363a4f",
|
||||
"ring": "#8aadf4",
|
||||
"border_transparent": "#cad3f500",
|
||||
"border_disabled": "#1e2030",
|
||||
"ring": "#b7bdf8",
|
||||
"text": "#cad3f5",
|
||||
"text_muted": "#b8c0e0",
|
||||
"text_placeholder": "#a5adcb",
|
||||
"text_muted": "#a5adcb",
|
||||
"text_placeholder": "#8087a2",
|
||||
"text_accent": "#8aadf4",
|
||||
"icon": "#b8c0e0",
|
||||
"icon_muted": "#a5adcb",
|
||||
"icon_accent": "#8aadf4",
|
||||
"element_foreground": "#181926",
|
||||
"text_danger": "#ed8796",
|
||||
"text_warning": "#f5a97f",
|
||||
"icon": "#a5adcb",
|
||||
"icon_muted": "#8087a2",
|
||||
"icon_accent": "#b7bdf8",
|
||||
"element_foreground": "#24273a",
|
||||
"element_background": "#8aadf4",
|
||||
"element_hover": "#b7bdf8",
|
||||
"element_active": "#7c9cdc",
|
||||
"element_selected": "#6e8bc5",
|
||||
"element_active": "#91d7e3",
|
||||
"element_selected": "#7dc4e4",
|
||||
"element_disabled": "#8aadf44d",
|
||||
"secondary_foreground": "#6e8bc5",
|
||||
"secondary_background": "#1e2030",
|
||||
"secondary_hover": "#8aadf433",
|
||||
"secondary_active": "#181926",
|
||||
"secondary_selected": "#181926",
|
||||
"secondary_foreground": "#cad3f5",
|
||||
"secondary_background": "#363a4f",
|
||||
"secondary_hover": "#494d64",
|
||||
"secondary_active": "#5b6078",
|
||||
"secondary_selected": "#5b6078",
|
||||
"secondary_disabled": "#8aadf44d",
|
||||
"danger_foreground": "#181926",
|
||||
"danger_foreground": "#24273a",
|
||||
"danger_background": "#ed8796",
|
||||
"danger_hover": "#ee99a0",
|
||||
"danger_active": "#d57a87",
|
||||
"danger_selected": "#be6d78",
|
||||
"danger_active": "#f5a97f",
|
||||
"danger_selected": "#eed49f",
|
||||
"danger_disabled": "#ed87964d",
|
||||
"warning_foreground": "#181926",
|
||||
"warning_background": "#eed49f",
|
||||
"warning_hover": "#f5a97f",
|
||||
"warning_active": "#d6bf8f",
|
||||
"warning_selected": "#beaa7f",
|
||||
"warning_disabled": "#eed49f4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#363a4f",
|
||||
"ghost_element_hover": "#cad3f533",
|
||||
"ghost_element_active": "#494d64",
|
||||
"ghost_element_selected": "#494d64",
|
||||
"ghost_element_disabled": "#cad3f50d",
|
||||
"tab_inactive_background": "#1e2030",
|
||||
"tab_inactive_foreground": "#b8c0e0",
|
||||
"warning_foreground": "#24273a",
|
||||
"warning_background": "#f5a97f",
|
||||
"warning_hover": "#eed49f",
|
||||
"warning_active": "#a6da95",
|
||||
"warning_selected": "#8bd5ca",
|
||||
"warning_disabled": "#f5a97f4d",
|
||||
"ghost_element_background": "#cad3f500",
|
||||
"ghost_element_background_alt": "#1e2030",
|
||||
"ghost_element_hover": "#cad3f50d",
|
||||
"ghost_element_active": "#cad3f51a",
|
||||
"ghost_element_selected": "#cad3f51a",
|
||||
"ghost_element_disabled": "#cad3f505",
|
||||
"tab_background": "#181926",
|
||||
"tab_foreground": "#a5adcb",
|
||||
"tab_hover_background": "#cad3f50d",
|
||||
"tab_active_background": "#24273a",
|
||||
"tab_active_foreground": "#cad3f5",
|
||||
"tab_hover_foreground": "#b7bdf8",
|
||||
"scrollbar_thumb_background": "#cad3f533",
|
||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#494d64",
|
||||
"scrollbar_thumb_background": "#cad3f51a",
|
||||
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||
"scrollbar_thumb_border": "#cad3f500",
|
||||
"scrollbar_track_background": "#cad3f500",
|
||||
"scrollbar_track_border": "#cad3f500",
|
||||
"drop_target_background": "#8aadf41a",
|
||||
"cursor": "#f4dbd6",
|
||||
"selection": "#939ab740"
|
||||
"cursor": "#8aadf4",
|
||||
"selection": "#8aadf440"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#24273a",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#181926",
|
||||
"panel_background": "#24273a",
|
||||
"overlay": "#cad3f51a",
|
||||
"title_bar": "#1e2030",
|
||||
"title_bar_inactive": "#181926",
|
||||
"window_border": "#6e738d",
|
||||
"border": "#5b6078",
|
||||
"border_variant": "#494d64",
|
||||
"title_bar": "#181926",
|
||||
"title_bar_inactive": "#24273a",
|
||||
"window_border": "#494d64",
|
||||
"border": "#494d64",
|
||||
"border_variant": "#363a4f",
|
||||
"border_focused": "#8aadf4",
|
||||
"border_selected": "#8aadf4",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#363a4f",
|
||||
"ring": "#8aadf4",
|
||||
"border_transparent": "#cad3f500",
|
||||
"border_disabled": "#1e2030",
|
||||
"ring": "#b7bdf8",
|
||||
"text": "#cad3f5",
|
||||
"text_muted": "#b8c0e0",
|
||||
"text_placeholder": "#a5adcb",
|
||||
"text_muted": "#a5adcb",
|
||||
"text_placeholder": "#8087a2",
|
||||
"text_accent": "#8aadf4",
|
||||
"icon": "#b8c0e0",
|
||||
"icon_muted": "#a5adcb",
|
||||
"icon_accent": "#8aadf4",
|
||||
"element_foreground": "#181926",
|
||||
"text_danger": "#ed8796",
|
||||
"text_warning": "#f5a97f",
|
||||
"icon": "#a5adcb",
|
||||
"icon_muted": "#8087a2",
|
||||
"icon_accent": "#b7bdf8",
|
||||
"element_foreground": "#24273a",
|
||||
"element_background": "#8aadf4",
|
||||
"element_hover": "#b7bdf8",
|
||||
"element_active": "#7c9cdc",
|
||||
"element_selected": "#6e8bc5",
|
||||
"element_active": "#91d7e3",
|
||||
"element_selected": "#7dc4e4",
|
||||
"element_disabled": "#8aadf44d",
|
||||
"secondary_foreground": "#6e8bc5",
|
||||
"secondary_background": "#1e2030",
|
||||
"secondary_hover": "#8aadf433",
|
||||
"secondary_active": "#181926",
|
||||
"secondary_selected": "#181926",
|
||||
"secondary_foreground": "#cad3f5",
|
||||
"secondary_background": "#363a4f",
|
||||
"secondary_hover": "#494d64",
|
||||
"secondary_active": "#5b6078",
|
||||
"secondary_selected": "#5b6078",
|
||||
"secondary_disabled": "#8aadf44d",
|
||||
"danger_foreground": "#181926",
|
||||
"danger_foreground": "#24273a",
|
||||
"danger_background": "#ed8796",
|
||||
"danger_hover": "#ee99a0",
|
||||
"danger_active": "#d57a87",
|
||||
"danger_selected": "#be6d78",
|
||||
"danger_active": "#f5a97f",
|
||||
"danger_selected": "#eed49f",
|
||||
"danger_disabled": "#ed87964d",
|
||||
"warning_foreground": "#181926",
|
||||
"warning_background": "#eed49f",
|
||||
"warning_hover": "#f5a97f",
|
||||
"warning_active": "#d6bf8f",
|
||||
"warning_selected": "#beaa7f",
|
||||
"warning_disabled": "#eed49f4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#363a4f",
|
||||
"ghost_element_hover": "#cad3f533",
|
||||
"ghost_element_active": "#494d64",
|
||||
"ghost_element_selected": "#494d64",
|
||||
"ghost_element_disabled": "#cad3f50d",
|
||||
"tab_inactive_background": "#1e2030",
|
||||
"tab_inactive_foreground": "#b8c0e0",
|
||||
"warning_foreground": "#24273a",
|
||||
"warning_background": "#f5a97f",
|
||||
"warning_hover": "#eed49f",
|
||||
"warning_active": "#a6da95",
|
||||
"warning_selected": "#8bd5ca",
|
||||
"warning_disabled": "#f5a97f4d",
|
||||
"ghost_element_background": "#cad3f500",
|
||||
"ghost_element_background_alt": "#1e2030",
|
||||
"ghost_element_hover": "#cad3f50d",
|
||||
"ghost_element_active": "#cad3f51a",
|
||||
"ghost_element_selected": "#cad3f51a",
|
||||
"ghost_element_disabled": "#cad3f505",
|
||||
"tab_background": "#181926",
|
||||
"tab_foreground": "#a5adcb",
|
||||
"tab_hover_background": "#cad3f50d",
|
||||
"tab_active_background": "#24273a",
|
||||
"tab_active_foreground": "#cad3f5",
|
||||
"tab_hover_foreground": "#b7bdf8",
|
||||
"scrollbar_thumb_background": "#cad3f533",
|
||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#494d64",
|
||||
"scrollbar_thumb_background": "#cad3f51a",
|
||||
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||
"scrollbar_thumb_border": "#cad3f500",
|
||||
"scrollbar_track_background": "#cad3f500",
|
||||
"scrollbar_track_border": "#cad3f500",
|
||||
"drop_target_background": "#8aadf41a",
|
||||
"cursor": "#f4dbd6",
|
||||
"selection": "#939ab740"
|
||||
"cursor": "#8aadf4",
|
||||
"selection": "#8aadf440"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-mocha",
|
||||
"name": "Catppuccin Mocha",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#1e1e2e",
|
||||
"surface_background": "#181825",
|
||||
"elevated_surface_background": "#11111b",
|
||||
"panel_background": "#1e1e2e",
|
||||
"overlay": "#cdd6f41a",
|
||||
"title_bar": "#181825",
|
||||
"title_bar_inactive": "#11111b",
|
||||
"window_border": "#6c7086",
|
||||
"border": "#585b70",
|
||||
"border_variant": "#45475a",
|
||||
"title_bar": "#11111b",
|
||||
"title_bar_inactive": "#1e1e2e",
|
||||
"window_border": "#45475a",
|
||||
"border": "#45475a",
|
||||
"border_variant": "#313244",
|
||||
"border_focused": "#89b4fa",
|
||||
"border_selected": "#89b4fa",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#313244",
|
||||
"ring": "#89b4fa",
|
||||
"border_transparent": "#cdd6f400",
|
||||
"border_disabled": "#181825",
|
||||
"ring": "#b4befe",
|
||||
"text": "#cdd6f4",
|
||||
"text_muted": "#bac2de",
|
||||
"text_placeholder": "#a6adc8",
|
||||
"text_muted": "#a6adc8",
|
||||
"text_placeholder": "#7f849c",
|
||||
"text_accent": "#89b4fa",
|
||||
"icon": "#bac2de",
|
||||
"icon_muted": "#a6adc8",
|
||||
"icon_accent": "#89b4fa",
|
||||
"element_foreground": "#11111b",
|
||||
"text_danger": "#f38ba8",
|
||||
"text_warning": "#fab387",
|
||||
"icon": "#a6adc8",
|
||||
"icon_muted": "#7f849c",
|
||||
"icon_accent": "#b4befe",
|
||||
"element_foreground": "#1e1e2e",
|
||||
"element_background": "#89b4fa",
|
||||
"element_hover": "#b4befe",
|
||||
"element_active": "#7ba2e1",
|
||||
"element_selected": "#6d90c9",
|
||||
"element_active": "#89dceb",
|
||||
"element_selected": "#74c7ec",
|
||||
"element_disabled": "#89b4fa4d",
|
||||
"secondary_foreground": "#6d90c9",
|
||||
"secondary_background": "#181825",
|
||||
"secondary_hover": "#89b4fa33",
|
||||
"secondary_active": "#11111b",
|
||||
"secondary_selected": "#11111b",
|
||||
"secondary_foreground": "#cdd6f4",
|
||||
"secondary_background": "#313244",
|
||||
"secondary_hover": "#45475a",
|
||||
"secondary_active": "#585b70",
|
||||
"secondary_selected": "#585b70",
|
||||
"secondary_disabled": "#89b4fa4d",
|
||||
"danger_foreground": "#11111b",
|
||||
"danger_foreground": "#1e1e2e",
|
||||
"danger_background": "#f38ba8",
|
||||
"danger_hover": "#eba0ac",
|
||||
"danger_active": "#db7d98",
|
||||
"danger_selected": "#c46f88",
|
||||
"danger_active": "#fab387",
|
||||
"danger_selected": "#f9e2af",
|
||||
"danger_disabled": "#f38ba84d",
|
||||
"warning_foreground": "#11111b",
|
||||
"warning_background": "#f9e2af",
|
||||
"warning_hover": "#fab387",
|
||||
"warning_active": "#e0cb9e",
|
||||
"warning_selected": "#c8b48d",
|
||||
"warning_disabled": "#f9e2af4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#313244",
|
||||
"ghost_element_hover": "#cdd6f433",
|
||||
"ghost_element_active": "#45475a",
|
||||
"ghost_element_selected": "#45475a",
|
||||
"ghost_element_disabled": "#cdd6f40d",
|
||||
"tab_inactive_background": "#181825",
|
||||
"tab_inactive_foreground": "#bac2de",
|
||||
"warning_foreground": "#1e1e2e",
|
||||
"warning_background": "#fab387",
|
||||
"warning_hover": "#f9e2af",
|
||||
"warning_active": "#a6e3a1",
|
||||
"warning_selected": "#94e2d5",
|
||||
"warning_disabled": "#fab3874d",
|
||||
"ghost_element_background": "#cdd6f400",
|
||||
"ghost_element_background_alt": "#181825",
|
||||
"ghost_element_hover": "#cdd6f40d",
|
||||
"ghost_element_active": "#cdd6f41a",
|
||||
"ghost_element_selected": "#cdd6f41a",
|
||||
"ghost_element_disabled": "#cdd6f405",
|
||||
"tab_background": "#11111b",
|
||||
"tab_foreground": "#a6adc8",
|
||||
"tab_hover_background": "#cdd6f40d",
|
||||
"tab_active_background": "#1e1e2e",
|
||||
"tab_active_foreground": "#cdd6f4",
|
||||
"tab_hover_foreground": "#b4befe",
|
||||
"scrollbar_thumb_background": "#cdd6f433",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#45475a",
|
||||
"scrollbar_thumb_background": "#cdd6f41a",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||
"scrollbar_thumb_border": "#cdd6f400",
|
||||
"scrollbar_track_background": "#cdd6f400",
|
||||
"scrollbar_track_border": "#cdd6f400",
|
||||
"drop_target_background": "#89b4fa1a",
|
||||
"cursor": "#f5e0dc",
|
||||
"selection": "#9399b240"
|
||||
"cursor": "#89b4fa",
|
||||
"selection": "#89b4fa40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#1e1e2e",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#11111b",
|
||||
"panel_background": "#1e1e2e",
|
||||
"overlay": "#cdd6f41a",
|
||||
"title_bar": "#181825",
|
||||
"title_bar_inactive": "#11111b",
|
||||
"window_border": "#6c7086",
|
||||
"border": "#585b70",
|
||||
"border_variant": "#45475a",
|
||||
"title_bar": "#11111b",
|
||||
"title_bar_inactive": "#1e1e2e",
|
||||
"window_border": "#45475a",
|
||||
"border": "#45475a",
|
||||
"border_variant": "#313244",
|
||||
"border_focused": "#89b4fa",
|
||||
"border_selected": "#89b4fa",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#313244",
|
||||
"ring": "#89b4fa",
|
||||
"border_transparent": "#cdd6f400",
|
||||
"border_disabled": "#181825",
|
||||
"ring": "#b4befe",
|
||||
"text": "#cdd6f4",
|
||||
"text_muted": "#bac2de",
|
||||
"text_placeholder": "#a6adc8",
|
||||
"text_muted": "#a6adc8",
|
||||
"text_placeholder": "#7f849c",
|
||||
"text_accent": "#89b4fa",
|
||||
"icon": "#bac2de",
|
||||
"icon_muted": "#a6adc8",
|
||||
"icon_accent": "#89b4fa",
|
||||
"element_foreground": "#11111b",
|
||||
"text_danger": "#f38ba8",
|
||||
"text_warning": "#fab387",
|
||||
"icon": "#a6adc8",
|
||||
"icon_muted": "#7f849c",
|
||||
"icon_accent": "#b4befe",
|
||||
"element_foreground": "#1e1e2e",
|
||||
"element_background": "#89b4fa",
|
||||
"element_hover": "#b4befe",
|
||||
"element_active": "#7ba2e1",
|
||||
"element_selected": "#6d90c9",
|
||||
"element_active": "#89dceb",
|
||||
"element_selected": "#74c7ec",
|
||||
"element_disabled": "#89b4fa4d",
|
||||
"secondary_foreground": "#6d90c9",
|
||||
"secondary_background": "#181825",
|
||||
"secondary_hover": "#89b4fa33",
|
||||
"secondary_active": "#11111b",
|
||||
"secondary_selected": "#11111b",
|
||||
"secondary_foreground": "#cdd6f4",
|
||||
"secondary_background": "#313244",
|
||||
"secondary_hover": "#45475a",
|
||||
"secondary_active": "#585b70",
|
||||
"secondary_selected": "#585b70",
|
||||
"secondary_disabled": "#89b4fa4d",
|
||||
"danger_foreground": "#11111b",
|
||||
"danger_foreground": "#1e1e2e",
|
||||
"danger_background": "#f38ba8",
|
||||
"danger_hover": "#eba0ac",
|
||||
"danger_active": "#db7d98",
|
||||
"danger_selected": "#c46f88",
|
||||
"danger_active": "#fab387",
|
||||
"danger_selected": "#f9e2af",
|
||||
"danger_disabled": "#f38ba84d",
|
||||
"warning_foreground": "#11111b",
|
||||
"warning_background": "#f9e2af",
|
||||
"warning_hover": "#fab387",
|
||||
"warning_active": "#e0cb9e",
|
||||
"warning_selected": "#c8b48d",
|
||||
"warning_disabled": "#f9e2af4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#313244",
|
||||
"ghost_element_hover": "#cdd6f433",
|
||||
"ghost_element_active": "#45475a",
|
||||
"ghost_element_selected": "#45475a",
|
||||
"ghost_element_disabled": "#cdd6f40d",
|
||||
"tab_inactive_background": "#181825",
|
||||
"tab_inactive_foreground": "#bac2de",
|
||||
"warning_foreground": "#1e1e2e",
|
||||
"warning_background": "#fab387",
|
||||
"warning_hover": "#f9e2af",
|
||||
"warning_active": "#a6e3a1",
|
||||
"warning_selected": "#94e2d5",
|
||||
"warning_disabled": "#fab3874d",
|
||||
"ghost_element_background": "#cdd6f400",
|
||||
"ghost_element_background_alt": "#181825",
|
||||
"ghost_element_hover": "#cdd6f40d",
|
||||
"ghost_element_active": "#cdd6f41a",
|
||||
"ghost_element_selected": "#cdd6f41a",
|
||||
"ghost_element_disabled": "#cdd6f405",
|
||||
"tab_background": "#11111b",
|
||||
"tab_foreground": "#a6adc8",
|
||||
"tab_hover_background": "#cdd6f40d",
|
||||
"tab_active_background": "#1e1e2e",
|
||||
"tab_active_foreground": "#cdd6f4",
|
||||
"tab_hover_foreground": "#b4befe",
|
||||
"scrollbar_thumb_background": "#cdd6f433",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#45475a",
|
||||
"scrollbar_thumb_background": "#cdd6f41a",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||
"scrollbar_thumb_border": "#cdd6f400",
|
||||
"scrollbar_track_background": "#cdd6f400",
|
||||
"scrollbar_track_border": "#cdd6f400",
|
||||
"drop_target_background": "#89b4fa1a",
|
||||
"cursor": "#f5e0dc",
|
||||
"selection": "#9399b240"
|
||||
"cursor": "#89b4fa",
|
||||
"selection": "#89b4fa40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,144 @@
|
||||
{
|
||||
"id": "flexoki",
|
||||
"name": "Flexoki",
|
||||
"author": "Stephan Ango",
|
||||
"author": "Steph Ango (ported by Coop)",
|
||||
"url": "https://stephango.com/flexoki",
|
||||
"light": {
|
||||
"background": "#FFFCF0",
|
||||
"surface_background": "#F2F0E5",
|
||||
"elevated_surface_background": "#E6E4D9",
|
||||
"panel_background": "#FFFCF0",
|
||||
"overlay": "#100F0F1a",
|
||||
"title_bar": "#F2F0E5",
|
||||
"title_bar_inactive": "#E6E4D9",
|
||||
"window_border": "#B7B5AC",
|
||||
"overlay": "#100F0F1A",
|
||||
"title_bar": "#E6E4D9",
|
||||
"title_bar_inactive": "#FFFCF0",
|
||||
"window_border": "#CECDC3",
|
||||
"border": "#CECDC3",
|
||||
"border_variant": "#DAD8CE",
|
||||
"border_focused": "#205EA6",
|
||||
"border_selected": "#205EA6",
|
||||
"border_transparent": "#00000000",
|
||||
"border_focused": "#24837B",
|
||||
"border_selected": "#24837B",
|
||||
"border_transparent": "#100F0F00",
|
||||
"border_disabled": "#E6E4D9",
|
||||
"ring": "#205EA6",
|
||||
"ring": "#3AA99F",
|
||||
"text": "#100F0F",
|
||||
"text_muted": "#6F6E69",
|
||||
"text_placeholder": "#9F9D96",
|
||||
"text_accent": "#205EA6",
|
||||
"text_placeholder": "#B7B5AC",
|
||||
"text_accent": "#24837B",
|
||||
"text_danger": "#AF3029",
|
||||
"text_warning": "#BC5215",
|
||||
"icon": "#6F6E69",
|
||||
"icon_muted": "#9F9D96",
|
||||
"icon_accent": "#205EA6",
|
||||
"icon_muted": "#B7B5AC",
|
||||
"icon_accent": "#3AA99F",
|
||||
"element_foreground": "#FFFCF0",
|
||||
"element_background": "#205EA6",
|
||||
"element_hover": "#1A4F8C",
|
||||
"element_active": "#163B66",
|
||||
"element_selected": "#133051",
|
||||
"element_disabled": "#205EA64d",
|
||||
"secondary_foreground": "#163B66",
|
||||
"secondary_background": "#F2F0E5",
|
||||
"secondary_hover": "#205EA61a",
|
||||
"secondary_active": "#E6E4D9",
|
||||
"secondary_selected": "#E6E4D9",
|
||||
"secondary_disabled": "#205EA64d",
|
||||
"element_background": "#24837B",
|
||||
"element_hover": "#3AA99F",
|
||||
"element_active": "#1C1B1A",
|
||||
"element_selected": "#100F0F",
|
||||
"element_disabled": "#24837B4D",
|
||||
"secondary_foreground": "#100F0F",
|
||||
"secondary_background": "#E6E4D9",
|
||||
"secondary_hover": "#DAD8CE",
|
||||
"secondary_active": "#CECDC3",
|
||||
"secondary_selected": "#CECDC3",
|
||||
"secondary_disabled": "#24837B4D",
|
||||
"danger_foreground": "#FFFCF0",
|
||||
"danger_background": "#D14D41",
|
||||
"danger_hover": "#C03E35",
|
||||
"danger_active": "#AF3029",
|
||||
"danger_selected": "#942822",
|
||||
"danger_disabled": "#D14D414d",
|
||||
"warning_foreground": "#100F0F",
|
||||
"warning_background": "#D0A215",
|
||||
"warning_hover": "#BE9207",
|
||||
"warning_active": "#AD8301",
|
||||
"warning_selected": "#8E6B01",
|
||||
"warning_disabled": "#D0A2154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#E6E4D9",
|
||||
"ghost_element_hover": "#100F0F1a",
|
||||
"ghost_element_active": "#DAD8CE",
|
||||
"ghost_element_selected": "#DAD8CE",
|
||||
"ghost_element_disabled": "#100F0F0d",
|
||||
"tab_inactive_background": "#F2F0E5",
|
||||
"tab_inactive_foreground": "#6F6E69",
|
||||
"danger_background": "#AF3029",
|
||||
"danger_hover": "#D14D41",
|
||||
"danger_active": "#1C1B1A",
|
||||
"danger_selected": "#100F0F",
|
||||
"danger_disabled": "#AF30294D",
|
||||
"warning_foreground": "#FFFCF0",
|
||||
"warning_background": "#BC5215",
|
||||
"warning_hover": "#DA702C",
|
||||
"warning_active": "#1C1B1A",
|
||||
"warning_selected": "#100F0F",
|
||||
"warning_disabled": "#BC52154D",
|
||||
"ghost_element_background": "#100F0F00",
|
||||
"ghost_element_background_alt": "#F2F0E5",
|
||||
"ghost_element_hover": "#100F0F0D",
|
||||
"ghost_element_active": "#100F0F1A",
|
||||
"ghost_element_selected": "#100F0F1A",
|
||||
"ghost_element_disabled": "#100F0F05",
|
||||
"tab_background": "#E6E4D9",
|
||||
"tab_foreground": "#6F6E69",
|
||||
"tab_hover_background": "#100F0F0D",
|
||||
"tab_active_background": "#FFFCF0",
|
||||
"tab_active_foreground": "#100F0F",
|
||||
"tab_hover_foreground": "#205EA6",
|
||||
"scrollbar_thumb_background": "#100F0F33",
|
||||
"scrollbar_thumb_hover_background": "#100F0F4d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#DAD8CE",
|
||||
"drop_target_background": "#205EA61a",
|
||||
"cursor": "#205EA6",
|
||||
"selection": "#205EA640"
|
||||
"scrollbar_thumb_background": "#100F0F1A",
|
||||
"scrollbar_thumb_hover_background": "#100F0F26",
|
||||
"scrollbar_thumb_border": "#100F0F00",
|
||||
"scrollbar_track_background": "#100F0F00",
|
||||
"scrollbar_track_border": "#100F0F00",
|
||||
"drop_target_background": "#24837B1A",
|
||||
"cursor": "#24837B",
|
||||
"selection": "#24837B40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#100F0F",
|
||||
"surface_background": "#1C1B1A",
|
||||
"elevated_surface_background": "#282726",
|
||||
"panel_background": "#100F0F",
|
||||
"overlay": "#FFFCF01a",
|
||||
"title_bar": "#1C1B1A",
|
||||
"title_bar_inactive": "#282726",
|
||||
"window_border": "#575653",
|
||||
"overlay": "#FFFCF01A",
|
||||
"title_bar": "#282726",
|
||||
"title_bar_inactive": "#100F0F",
|
||||
"window_border": "#403E3C",
|
||||
"border": "#403E3C",
|
||||
"border_variant": "#343331",
|
||||
"border_focused": "#4385BE",
|
||||
"border_selected": "#4385BE",
|
||||
"border_transparent": "#00000000",
|
||||
"border_focused": "#3AA99F",
|
||||
"border_selected": "#3AA99F",
|
||||
"border_transparent": "#100F0F00",
|
||||
"border_disabled": "#282726",
|
||||
"ring": "#4385BE",
|
||||
"text": "#FFFCF0",
|
||||
"ring": "#24837B",
|
||||
"text": "#CECDC3",
|
||||
"text_muted": "#878580",
|
||||
"text_placeholder": "#6F6E69",
|
||||
"text_accent": "#4385BE",
|
||||
"text_placeholder": "#575653",
|
||||
"text_accent": "#3AA99F",
|
||||
"text_danger": "#D14D41",
|
||||
"text_warning": "#DA702C",
|
||||
"icon": "#878580",
|
||||
"icon_muted": "#6F6E69",
|
||||
"icon_accent": "#4385BE",
|
||||
"icon_muted": "#575653",
|
||||
"icon_accent": "#24837B",
|
||||
"element_foreground": "#100F0F",
|
||||
"element_background": "#4385BE",
|
||||
"element_hover": "#3171B2",
|
||||
"element_active": "#205EA6",
|
||||
"element_selected": "#1A4F8C",
|
||||
"element_disabled": "#4385BE4d",
|
||||
"secondary_foreground": "#205EA6",
|
||||
"element_background": "#3AA99F",
|
||||
"element_hover": "#24837B",
|
||||
"element_active": "#CECDC3",
|
||||
"element_selected": "#F2F0E5",
|
||||
"element_disabled": "#3AA99F4D",
|
||||
"secondary_foreground": "#CECDC3",
|
||||
"secondary_background": "#1C1B1A",
|
||||
"secondary_hover": "#4385BE1a",
|
||||
"secondary_active": "#282726",
|
||||
"secondary_selected": "#282726",
|
||||
"secondary_disabled": "#4385BE4d",
|
||||
"secondary_hover": "#282726",
|
||||
"secondary_active": "#343331",
|
||||
"secondary_selected": "#343331",
|
||||
"secondary_disabled": "#3AA99F4D",
|
||||
"danger_foreground": "#100F0F",
|
||||
"danger_background": "#E8705F",
|
||||
"danger_hover": "#D14D41",
|
||||
"danger_active": "#C03E35",
|
||||
"danger_selected": "#AF3029",
|
||||
"danger_disabled": "#E8705F4d",
|
||||
"danger_background": "#D14D41",
|
||||
"danger_hover": "#AF3029",
|
||||
"danger_active": "#CECDC3",
|
||||
"danger_selected": "#F2F0E5",
|
||||
"danger_disabled": "#D14D414D",
|
||||
"warning_foreground": "#100F0F",
|
||||
"warning_background": "#DFB431",
|
||||
"warning_hover": "#D0A215",
|
||||
"warning_active": "#BE9207",
|
||||
"warning_selected": "#AD8301",
|
||||
"warning_disabled": "#DFB4314d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#282726",
|
||||
"ghost_element_hover": "#FFFCF01a",
|
||||
"ghost_element_active": "#343331",
|
||||
"ghost_element_selected": "#343331",
|
||||
"ghost_element_disabled": "#FFFCF00d",
|
||||
"tab_inactive_background": "#1C1B1A",
|
||||
"tab_inactive_foreground": "#878580",
|
||||
"warning_background": "#DA702C",
|
||||
"warning_hover": "#BC5215",
|
||||
"warning_active": "#CECDC3",
|
||||
"warning_selected": "#F2F0E5",
|
||||
"warning_disabled": "#DA702C4D",
|
||||
"ghost_element_background": "#100F0F00",
|
||||
"ghost_element_background_alt": "#1C1B1A",
|
||||
"ghost_element_hover": "#FFFCF00D",
|
||||
"ghost_element_active": "#FFFCF01A",
|
||||
"ghost_element_selected": "#FFFCF01A",
|
||||
"ghost_element_disabled": "#FFFCF005",
|
||||
"tab_background": "#282726",
|
||||
"tab_foreground": "#878580",
|
||||
"tab_hover_background": "#FFFCF00D",
|
||||
"tab_active_background": "#100F0F",
|
||||
"tab_active_foreground": "#FFFCF0",
|
||||
"tab_hover_foreground": "#4385BE",
|
||||
"scrollbar_thumb_background": "#FFFCF033",
|
||||
"scrollbar_thumb_hover_background": "#FFFCF04d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#343331",
|
||||
"drop_target_background": "#4385BE1a",
|
||||
"cursor": "#4385BE",
|
||||
"selection": "#4385BE40"
|
||||
"tab_active_foreground": "#CECDC3",
|
||||
"scrollbar_thumb_background": "#FFFCF01A",
|
||||
"scrollbar_thumb_hover_background": "#FFFCF026",
|
||||
"scrollbar_thumb_border": "#100F0F00",
|
||||
"scrollbar_track_background": "#100F0F00",
|
||||
"scrollbar_track_border": "#100F0F00",
|
||||
"drop_target_background": "#3AA99F1A",
|
||||
"cursor": "#3AA99F",
|
||||
"selection": "#3AA99F40"
|
||||
}
|
||||
}
|
||||
|
||||
144
assets/themes/forest.json
Normal file
144
assets/themes/forest.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "forest",
|
||||
"name": "Forest",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fbfefcff",
|
||||
"surface_background": "#f4fbf6ff",
|
||||
"elevated_surface_background": "#e9f6e9ff",
|
||||
"panel_background": "#fbfefcff",
|
||||
"overlay": "#193b2d1a",
|
||||
"title_bar": "#e9f6e9ff",
|
||||
"title_bar_inactive": "#fbfefcff",
|
||||
"window_border": "#c4e8d1ff",
|
||||
"border": "#c4e8d1ff",
|
||||
"border_variant": "#b2ddb5ff",
|
||||
"border_focused": "#30a46cff",
|
||||
"border_selected": "#30a46cff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e0f3e6ff",
|
||||
"ring": "#2b9a66ff",
|
||||
"text": "#193b2dff",
|
||||
"text_muted": "#2f7c57ff",
|
||||
"text_placeholder": "#8eceaaff",
|
||||
"text_accent": "#30a46cff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#2f7c57ff",
|
||||
"icon_muted": "#8eceaaff",
|
||||
"icon_accent": "#2b9a66ff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#30a46cff",
|
||||
"element_hover": "#2b9a66ff",
|
||||
"element_active": "#2a7e3bff",
|
||||
"element_selected": "#218358ff",
|
||||
"element_disabled": "#30a46c4d",
|
||||
"secondary_foreground": "#193b2dff",
|
||||
"secondary_background": "#e9f6e9ff",
|
||||
"secondary_hover": "#daf1dbff",
|
||||
"secondary_active": "#c4e8d1ff",
|
||||
"secondary_selected": "#c4e8d1ff",
|
||||
"secondary_disabled": "#30a46c4d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e9f6e9ff",
|
||||
"ghost_element_hover": "#193b2d0d",
|
||||
"ghost_element_active": "#193b2d1a",
|
||||
"ghost_element_selected": "#193b2d1a",
|
||||
"ghost_element_disabled": "#193b2d05",
|
||||
"tab_background": "#e9f6e9ff",
|
||||
"tab_foreground": "#2f7c57ff",
|
||||
"tab_hover_background": "#193b2d0d",
|
||||
"tab_active_background": "#fbfefcff",
|
||||
"tab_active_foreground": "#193b2dff",
|
||||
"scrollbar_thumb_background": "#193b2d1a",
|
||||
"scrollbar_thumb_hover_background": "#193b2d26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#30a46c1a",
|
||||
"cursor": "#30a46cff",
|
||||
"selection": "#30a46c40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0e1512ff",
|
||||
"surface_background": "#121b17ff",
|
||||
"elevated_surface_background": "#132d21ff",
|
||||
"panel_background": "#0e1512ff",
|
||||
"overlay": "#b1f1cb1a",
|
||||
"title_bar": "#132d21ff",
|
||||
"title_bar_inactive": "#0e1512ff",
|
||||
"window_border": "#20573eff",
|
||||
"border": "#20573eff",
|
||||
"border_variant": "#174933ff",
|
||||
"border_focused": "#33b074ff",
|
||||
"border_selected": "#33b074ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#113b29ff",
|
||||
"ring": "#33b074ff",
|
||||
"text": "#b1f1cbff",
|
||||
"text_muted": "#71d083ff",
|
||||
"text_placeholder": "#2f7c57ff",
|
||||
"text_accent": "#3dd68cff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#71d083ff",
|
||||
"icon_muted": "#2f7c57ff",
|
||||
"icon_accent": "#33b074ff",
|
||||
"element_foreground": "#0e1512ff",
|
||||
"element_background": "#3dd68cff",
|
||||
"element_hover": "#71d083ff",
|
||||
"element_active": "#33b074ff",
|
||||
"element_selected": "#30a46cff",
|
||||
"element_disabled": "#3dd68c4d",
|
||||
"secondary_foreground": "#b1f1cbff",
|
||||
"secondary_background": "#132d21ff",
|
||||
"secondary_hover": "#113b29ff",
|
||||
"secondary_active": "#174933ff",
|
||||
"secondary_selected": "#174933ff",
|
||||
"secondary_disabled": "#3dd68c4d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#132d21ff",
|
||||
"ghost_element_hover": "#b1f1cb0d",
|
||||
"ghost_element_active": "#b1f1cb1a",
|
||||
"ghost_element_selected": "#b1f1cb1a",
|
||||
"ghost_element_disabled": "#b1f1cb05",
|
||||
"tab_background": "#132d21ff",
|
||||
"tab_foreground": "#71d083ff",
|
||||
"tab_hover_background": "#b1f1cb0d",
|
||||
"tab_active_background": "#0e1512ff",
|
||||
"tab_active_foreground": "#b1f1cbff",
|
||||
"scrollbar_thumb_background": "#b1f1cb1a",
|
||||
"scrollbar_thumb_hover_background": "#b1f1cb26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#3dd68c1a",
|
||||
"cursor": "#3dd68cff",
|
||||
"selection": "#3dd68c40"
|
||||
}
|
||||
}
|
||||
144
assets/themes/ocean.json
Normal file
144
assets/themes/ocean.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "ocean",
|
||||
"name": "Ocean",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fafefeff",
|
||||
"surface_background": "#f2fbfaff",
|
||||
"elevated_surface_background": "#e6f7f7ff",
|
||||
"panel_background": "#fafefeff",
|
||||
"overlay": "#00333f1a",
|
||||
"title_bar": "#e6f7f7ff",
|
||||
"title_bar_inactive": "#fafefeff",
|
||||
"window_border": "#cce5e9ff",
|
||||
"border": "#cce5e9ff",
|
||||
"border_variant": "#b8dde3ff",
|
||||
"border_focused": "#00a2c7ff",
|
||||
"border_selected": "#00a2c7ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e0f0f2ff",
|
||||
"ring": "#0797b9ff",
|
||||
"text": "#0d3c48ff",
|
||||
"text_muted": "#107d98ff",
|
||||
"text_placeholder": "#60b3d7ff",
|
||||
"text_accent": "#00a2c7ff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#107d98ff",
|
||||
"icon_muted": "#60b3d7ff",
|
||||
"icon_accent": "#0797b9ff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#00a2c7ff",
|
||||
"element_hover": "#0797b9ff",
|
||||
"element_active": "#12667eff",
|
||||
"element_selected": "#0d4a5cff",
|
||||
"element_disabled": "#00a2c74d",
|
||||
"secondary_foreground": "#0d4a5cff",
|
||||
"secondary_background": "#ddf9f2ff",
|
||||
"secondary_hover": "#c8f4e9ff",
|
||||
"secondary_active": "#b3ecdeff",
|
||||
"secondary_selected": "#b3ecdeff",
|
||||
"secondary_disabled": "#00a2c74d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e6f7f7ff",
|
||||
"ghost_element_hover": "#00333f0d",
|
||||
"ghost_element_active": "#00333f1a",
|
||||
"ghost_element_selected": "#00333f1a",
|
||||
"ghost_element_disabled": "#00333f05",
|
||||
"tab_background": "#e6f7f7ff",
|
||||
"tab_foreground": "#107d98ff",
|
||||
"tab_hover_background": "#00333f0d",
|
||||
"tab_active_background": "#fafefeff",
|
||||
"tab_active_foreground": "#0d3c48ff",
|
||||
"scrollbar_thumb_background": "#00333f1a",
|
||||
"scrollbar_thumb_hover_background": "#00333f26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#00a2c71a",
|
||||
"cursor": "#00a2c7ff",
|
||||
"selection": "#00a2c740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0b161aff",
|
||||
"surface_background": "#101b20ff",
|
||||
"elevated_surface_background": "#082c36ff",
|
||||
"panel_background": "#0b161aff",
|
||||
"overlay": "#c2f3ff1a",
|
||||
"title_bar": "#082c36ff",
|
||||
"title_bar_inactive": "#0b161aff",
|
||||
"window_border": "#1b537bff",
|
||||
"border": "#1b537bff",
|
||||
"border_variant": "#154467ff",
|
||||
"border_focused": "#00a2c7ff",
|
||||
"border_selected": "#00a2c7ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#112840ff",
|
||||
"ring": "#23afd0ff",
|
||||
"text": "#b6ecf7ff",
|
||||
"text_muted": "#4ccce6ff",
|
||||
"text_placeholder": "#197caeff",
|
||||
"text_accent": "#7ce2feff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#4ccce6ff",
|
||||
"icon_muted": "#197caeff",
|
||||
"icon_accent": "#23afd0ff",
|
||||
"element_foreground": "#0b161aff",
|
||||
"element_background": "#7ce2feff",
|
||||
"element_hover": "#a8eeffff",
|
||||
"element_active": "#23afd0ff",
|
||||
"element_selected": "#00a2c7ff",
|
||||
"element_disabled": "#7ce2fe4d",
|
||||
"secondary_foreground": "#adf0ddff",
|
||||
"secondary_background": "#0d2d2aff",
|
||||
"secondary_hover": "#023b37ff",
|
||||
"secondary_active": "#084843ff",
|
||||
"secondary_selected": "#084843ff",
|
||||
"secondary_disabled": "#7ce2fe4d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#082c36ff",
|
||||
"ghost_element_hover": "#c2f3ff0d",
|
||||
"ghost_element_active": "#c2f3ff1a",
|
||||
"ghost_element_selected": "#c2f3ff1a",
|
||||
"ghost_element_disabled": "#c2f3ff05",
|
||||
"tab_background": "#082c36ff",
|
||||
"tab_foreground": "#4ccce6ff",
|
||||
"tab_hover_background": "#c2f3ff0d",
|
||||
"tab_active_background": "#0b161aff",
|
||||
"tab_active_foreground": "#b6ecf7ff",
|
||||
"scrollbar_thumb_background": "#c2f3ff1a",
|
||||
"scrollbar_thumb_hover_background": "#c2f3ff26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#7ce2fe1a",
|
||||
"cursor": "#7ce2feff",
|
||||
"selection": "#7ce2fe40"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
{
|
||||
"id": "rose-pine-dawn",
|
||||
"name": "Rosé Pine Dawn",
|
||||
"author": "Rosé Pine",
|
||||
"url": "https://rosepinetheme.com/",
|
||||
"light": {
|
||||
"background": "#faf4ed",
|
||||
"surface_background": "#fffaf3",
|
||||
"elevated_surface_background": "#f2e9e1",
|
||||
"panel_background": "#faf4ed",
|
||||
"overlay": "#5752791a",
|
||||
"title_bar": "#fffaf3",
|
||||
"title_bar_inactive": "#f2e9e1",
|
||||
"window_border": "#cecacd",
|
||||
"border": "#dfdad9",
|
||||
"border_variant": "#f4ede8",
|
||||
"border_focused": "#907aa9",
|
||||
"border_selected": "#907aa9",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#f2e9e1",
|
||||
"ring": "#907aa9",
|
||||
"text": "#575279",
|
||||
"text_muted": "#797593",
|
||||
"text_placeholder": "#9893a5",
|
||||
"text_accent": "#907aa9",
|
||||
"icon": "#797593",
|
||||
"icon_muted": "#9893a5",
|
||||
"icon_accent": "#907aa9",
|
||||
"element_foreground": "#faf4ed",
|
||||
"element_background": "#907aa9",
|
||||
"element_hover": "#907aa9e6",
|
||||
"element_active": "#826b95",
|
||||
"element_selected": "#745c81",
|
||||
"element_disabled": "#907aa94d",
|
||||
"secondary_foreground": "#745c81",
|
||||
"secondary_background": "#fffaf3",
|
||||
"secondary_hover": "#907aa91a",
|
||||
"secondary_active": "#f2e9e1",
|
||||
"secondary_selected": "#f2e9e1",
|
||||
"secondary_disabled": "#907aa94d",
|
||||
"danger_foreground": "#faf4ed",
|
||||
"danger_background": "#b4637a",
|
||||
"danger_hover": "#a7586e",
|
||||
"danger_active": "#9a4d62",
|
||||
"danger_selected": "#8d4256",
|
||||
"danger_disabled": "#b4637a4d",
|
||||
"warning_foreground": "#575279",
|
||||
"warning_background": "#ea9d34",
|
||||
"warning_hover": "#d98e2f",
|
||||
"warning_active": "#c87f2a",
|
||||
"warning_selected": "#b77025",
|
||||
"warning_disabled": "#ea9d344d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f2e9e1",
|
||||
"ghost_element_hover": "#5752791a",
|
||||
"ghost_element_active": "#dfdad9",
|
||||
"ghost_element_selected": "#dfdad9",
|
||||
"ghost_element_disabled": "#5752790d",
|
||||
"tab_inactive_background": "#fffaf3",
|
||||
"tab_inactive_foreground": "#797593",
|
||||
"tab_active_background": "#faf4ed",
|
||||
"tab_active_foreground": "#575279",
|
||||
"tab_hover_foreground": "#907aa9",
|
||||
"scrollbar_thumb_background": "#57527933",
|
||||
"scrollbar_thumb_hover_background": "#5752794d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#dfdad9",
|
||||
"drop_target_background": "#907aa91a",
|
||||
"cursor": "#907aa9",
|
||||
"selection": "#907aa940"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#faf4ed",
|
||||
"surface_background": "#fffaf3",
|
||||
"elevated_surface_background": "#f2e9e1",
|
||||
"panel_background": "#faf4ed",
|
||||
"overlay": "#5752791a",
|
||||
"title_bar": "#fffaf3",
|
||||
"title_bar_inactive": "#f2e9e1",
|
||||
"window_border": "#cecacd",
|
||||
"border": "#dfdad9",
|
||||
"border_variant": "#f4ede8",
|
||||
"border_focused": "#907aa9",
|
||||
"border_selected": "#907aa9",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#f2e9e1",
|
||||
"ring": "#907aa9",
|
||||
"text": "#575279",
|
||||
"text_muted": "#797593",
|
||||
"text_placeholder": "#9893a5",
|
||||
"text_accent": "#907aa9",
|
||||
"icon": "#797593",
|
||||
"icon_muted": "#9893a5",
|
||||
"icon_accent": "#907aa9",
|
||||
"element_foreground": "#faf4ed",
|
||||
"element_background": "#907aa9",
|
||||
"element_hover": "#907aa9e6",
|
||||
"element_active": "#826b95",
|
||||
"element_selected": "#745c81",
|
||||
"element_disabled": "#907aa94d",
|
||||
"secondary_foreground": "#745c81",
|
||||
"secondary_background": "#fffaf3",
|
||||
"secondary_hover": "#907aa91a",
|
||||
"secondary_active": "#f2e9e1",
|
||||
"secondary_selected": "#f2e9e1",
|
||||
"secondary_disabled": "#907aa94d",
|
||||
"danger_foreground": "#faf4ed",
|
||||
"danger_background": "#b4637a",
|
||||
"danger_hover": "#a7586e",
|
||||
"danger_active": "#9a4d62",
|
||||
"danger_selected": "#8d4256",
|
||||
"danger_disabled": "#b4637a4d",
|
||||
"warning_foreground": "#575279",
|
||||
"warning_background": "#ea9d34",
|
||||
"warning_hover": "#d98e2f",
|
||||
"warning_active": "#c87f2a",
|
||||
"warning_selected": "#b77025",
|
||||
"warning_disabled": "#ea9d344d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f2e9e1",
|
||||
"ghost_element_hover": "#5752791a",
|
||||
"ghost_element_active": "#dfdad9",
|
||||
"ghost_element_selected": "#dfdad9",
|
||||
"ghost_element_disabled": "#5752790d",
|
||||
"tab_inactive_background": "#fffaf3",
|
||||
"tab_inactive_foreground": "#797593",
|
||||
"tab_active_background": "#faf4ed",
|
||||
"tab_active_foreground": "#575279",
|
||||
"tab_hover_foreground": "#907aa9",
|
||||
"scrollbar_thumb_background": "#57527933",
|
||||
"scrollbar_thumb_hover_background": "#5752794d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#dfdad9",
|
||||
"drop_target_background": "#907aa91a",
|
||||
"cursor": "#907aa9",
|
||||
"selection": "#907aa940"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
{
|
||||
"id": "rose-pine-moon",
|
||||
"name": "Rosé Pine Moon",
|
||||
"author": "Rosé Pine",
|
||||
"url": "https://rosepinetheme.com/",
|
||||
"light": {
|
||||
"background": "#232136",
|
||||
"surface_background": "#2a273f",
|
||||
"elevated_surface_background": "#393552",
|
||||
"panel_background": "#232136",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#2a273f",
|
||||
"title_bar_inactive": "#393552",
|
||||
"window_border": "#56526e",
|
||||
"border": "#44415a",
|
||||
"border_variant": "#393552",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#393552",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#232136",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#393552",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#44415a",
|
||||
"secondary_selected": "#44415a",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#232136",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#232136",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#393552",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#44415a",
|
||||
"ghost_element_selected": "#44415a",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#2a273f",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#232136",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#44415a",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#232136",
|
||||
"surface_background": "#2a273f",
|
||||
"elevated_surface_background": "#393552",
|
||||
"panel_background": "#232136",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#2a273f",
|
||||
"title_bar_inactive": "#393552",
|
||||
"window_border": "#56526e",
|
||||
"border": "#44415a",
|
||||
"border_variant": "#393552",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#393552",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#232136",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#393552",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#44415a",
|
||||
"secondary_selected": "#44415a",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#232136",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#232136",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#393552",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#44415a",
|
||||
"ghost_element_selected": "#44415a",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#2a273f",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#232136",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#44415a",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
{
|
||||
"id": "rose-pine",
|
||||
"name": "Rosé Pine",
|
||||
"author": "Rosé Pine",
|
||||
"url": "https://rosepinetheme.com/",
|
||||
"light": {
|
||||
"background": "#191724",
|
||||
"surface_background": "#1f1d2e",
|
||||
"elevated_surface_background": "#26233a",
|
||||
"panel_background": "#191724",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#1f1d2e",
|
||||
"title_bar_inactive": "#26233a",
|
||||
"window_border": "#524f67",
|
||||
"border": "#403d52",
|
||||
"border_variant": "#26233a",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#26233a",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#191724",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#26233a",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#403d52",
|
||||
"secondary_selected": "#403d52",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#191724",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#191724",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#26233a",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#403d52",
|
||||
"ghost_element_selected": "#403d52",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#1f1d2e",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#191724",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#403d52",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#191724",
|
||||
"surface_background": "#1f1d2e",
|
||||
"elevated_surface_background": "#26233a",
|
||||
"panel_background": "#191724",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#1f1d2e",
|
||||
"title_bar_inactive": "#26233a",
|
||||
"window_border": "#524f67",
|
||||
"border": "#403d52",
|
||||
"border_variant": "#26233a",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#26233a",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#191724",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#26233a",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#403d52",
|
||||
"secondary_selected": "#403d52",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#191724",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#191724",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#26233a",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#403d52",
|
||||
"ghost_element_selected": "#403d52",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#1f1d2e",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#191724",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#403d52",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ use common::EventUtils;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, RelayState, TIMEOUT, USER_GIFTWRAP};
|
||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP};
|
||||
|
||||
mod message;
|
||||
mod room;
|
||||
@@ -39,6 +40,10 @@ pub enum ChatEvent {
|
||||
CloseRoom(u64),
|
||||
/// An event to notify UI about a new chat request
|
||||
Ping,
|
||||
/// An event to notify UI that the chat registry has subscribed to messaging relays
|
||||
Subscribed,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Channel signal.
|
||||
@@ -48,41 +53,25 @@ enum Signal {
|
||||
Message(NewMessage),
|
||||
/// Eose received from relay pool
|
||||
Eose,
|
||||
}
|
||||
|
||||
/// Inbox state.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InboxState {
|
||||
#[default]
|
||||
Idle,
|
||||
Checking,
|
||||
RelayNotAvailable,
|
||||
RelayConfigured(Box<Event>),
|
||||
Subscribing,
|
||||
}
|
||||
|
||||
impl InboxState {
|
||||
pub fn not_configured(&self) -> bool {
|
||||
matches!(self, InboxState::RelayNotAvailable)
|
||||
}
|
||||
|
||||
pub fn subscribing(&self) -> bool {
|
||||
matches!(self, InboxState::Subscribing)
|
||||
}
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Chat Registry
|
||||
#[derive(Debug)]
|
||||
pub struct ChatRegistry {
|
||||
/// Relay state for messaging relay list
|
||||
state: Entity<InboxState>,
|
||||
|
||||
/// Collection of all chat rooms
|
||||
rooms: Vec<Entity<Room>>,
|
||||
|
||||
/// Tracking the status of unwrapping gift wrap events.
|
||||
tracking_flag: Arc<AtomicBool>,
|
||||
|
||||
/// Channel for sending signals to the UI.
|
||||
signal_tx: flume::Sender<Signal>,
|
||||
|
||||
/// Channel for receiving signals from the UI.
|
||||
signal_rx: flume::Receiver<Signal>,
|
||||
|
||||
/// Async tasks
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||
|
||||
@@ -105,36 +94,18 @@ impl ChatRegistry {
|
||||
|
||||
/// Create a new chat registry instance
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let state = cx.new(|_| InboxState::default());
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let (tx, rx) = flume::unbounded::<Signal>();
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the nip65 state and load chat rooms on every state change
|
||||
cx.observe(&nostr, |this, state, cx| {
|
||||
match state.read(cx).relay_list_state {
|
||||
RelayState::Idle => {
|
||||
// Subscribe to the signer event
|
||||
cx.subscribe(&nostr, |this, _state, event, cx| {
|
||||
if let StateEvent::SignerSet = event {
|
||||
this.reset(cx);
|
||||
}
|
||||
RelayState::Configured => {
|
||||
this.get_contact_list(cx);
|
||||
this.ensure_messaging_relays(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Load rooms on every state change
|
||||
this.get_rooms(cx);
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the nip17 state and load chat rooms on every state change
|
||||
cx.observe(&state, |this, state, cx| {
|
||||
if let InboxState::RelayConfigured(event) = state.read(cx) {
|
||||
let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
|
||||
this.get_messages(relay_urls, cx);
|
||||
this.get_contact_list(cx);
|
||||
this.get_messages(cx)
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -147,9 +118,10 @@ impl ChatRegistry {
|
||||
});
|
||||
|
||||
Self {
|
||||
state,
|
||||
rooms: vec![],
|
||||
tracking_flag: Arc::new(AtomicBool::new(false)),
|
||||
signal_rx: rx,
|
||||
signal_tx: tx,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -167,7 +139,8 @@ impl ChatRegistry {
|
||||
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Signal>(1024);
|
||||
let tx = self.signal_tx.clone();
|
||||
let rx = self.signal_rx.clone();
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let device_signer = signer.get_encryption_signer().await;
|
||||
@@ -194,7 +167,14 @@ impl ChatRegistry {
|
||||
|
||||
// Extract the rumor from the gift wrap event
|
||||
match extract_rumor(&client, &device_signer, event.as_ref()).await {
|
||||
Ok(rumor) => match rumor.created_at >= initialized_at {
|
||||
Ok(rumor) => {
|
||||
if rumor.tags.is_empty() {
|
||||
let error: SharedString =
|
||||
"Message doesn't belong to any rooms".into();
|
||||
tx.send_async(Signal::Error(error)).await?;
|
||||
}
|
||||
|
||||
match rumor.created_at >= initialized_at {
|
||||
true => {
|
||||
let new_message = NewMessage::new(event.id, rumor);
|
||||
let signal = Signal::Message(new_message);
|
||||
@@ -204,9 +184,12 @@ impl ChatRegistry {
|
||||
false => {
|
||||
status.store(true, Ordering::Release);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to unwrap the gift wrap event: {e}");
|
||||
let error: SharedString =
|
||||
format!("Failed to unwrap the gift wrap event: {e}").into();
|
||||
tx.send_async(Signal::Error(error)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,6 +218,11 @@ impl ChatRegistry {
|
||||
this.get_rooms(cx);
|
||||
})?;
|
||||
}
|
||||
Signal::Error(error) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Error(error));
|
||||
})?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +233,7 @@ impl ChatRegistry {
|
||||
/// Tracking the status of unwrapping gift wrap events.
|
||||
fn tracking(&mut self, cx: &mut Context<Self>) {
|
||||
let status = self.tracking_flag.clone();
|
||||
let tx = self.signal_tx.clone();
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let loop_duration = Duration::from_secs(15);
|
||||
@@ -252,6 +241,9 @@ impl ChatRegistry {
|
||||
loop {
|
||||
if status.load(Ordering::Acquire) {
|
||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
||||
_ = tx.send_async(Signal::Eose).await;
|
||||
} else {
|
||||
_ = tx.send_async(Signal::Eose).await;
|
||||
}
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
@@ -268,29 +260,20 @@ impl ChatRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let id = SubscriptionId::new("contact-list");
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||
|
||||
// Get user's write relays
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ContactList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe
|
||||
client.subscribe(target).close_on(opts).with_id(id).await?;
|
||||
client.subscribe(filter).close_on(opts).with_id(id).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -298,39 +281,35 @@ impl ChatRegistry {
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
/// Ensure messaging relays are set up for the current user.
|
||||
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.verify_relays(cx);
|
||||
|
||||
// Set state to checking
|
||||
self.set_state(InboxState::Checking, cx);
|
||||
/// Get all messages for current user
|
||||
pub fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.subscribe(cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_state(result, cx);
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Subscribed);
|
||||
})?;
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify messaging relay list for current user
|
||||
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
|
||||
// Get messaging relay list for current user
|
||||
fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let filter = Filter::new()
|
||||
@@ -338,61 +317,32 @@ impl ChatRegistry {
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Stream events from user's write relays
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
return Ok(InboxState::RelayConfigured(Box::new(event)));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
if let Ok(event) = res {
|
||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||
return Ok(urls);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InboxState::RelayNotAvailable)
|
||||
Err(anyhow!("Messaging Relays not found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all messages for current user
|
||||
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
let task = self.subscribe(relay_urls, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_state(InboxState::Subscribing, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
let urls = urls.into_iter().collect::<Vec<_>>();
|
||||
let urls = self.get_messaging_relays(cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = urls.await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
let id = SubscriptionId::new(USER_GIFTWRAP);
|
||||
@@ -419,19 +369,6 @@ impl ChatRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the state of the inbox
|
||||
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
|
||||
self.state.update(cx, |this, cx| {
|
||||
*this = state;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the relay state
|
||||
pub fn state(&self, cx: &App) -> InboxState {
|
||||
self.state.read(cx).clone()
|
||||
}
|
||||
|
||||
/// Get the loading status of the chat registry
|
||||
pub fn loading(&self) -> bool {
|
||||
self.tracking_flag.load(Ordering::Acquire)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -10,7 +9,7 @@ use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
use settings::{RoomConfig, SignerKind};
|
||||
use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
||||
use state::{NostrRegistry, TIMEOUT};
|
||||
|
||||
use crate::NewMessage;
|
||||
|
||||
@@ -333,9 +332,6 @@ impl Room {
|
||||
let signer = nostr.read(cx).signer();
|
||||
let sender = signer.public_key();
|
||||
|
||||
// Get room's id
|
||||
let id = self.id;
|
||||
|
||||
// Get all members, excluding the sender
|
||||
let members: Vec<PublicKey> = self
|
||||
.members
|
||||
@@ -345,30 +341,27 @@ impl Room {
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let id = SubscriptionId::new(format!("room-{id}"));
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||
|
||||
// Construct filters for each member
|
||||
let filters: Vec<Filter> = members
|
||||
.into_iter()
|
||||
.map(|public_key| {
|
||||
Filter::new()
|
||||
for public_key in members.into_iter() {
|
||||
let inbox = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1)
|
||||
})
|
||||
.collect();
|
||||
.kind(Kind::InboxRelays)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, filters.clone()))
|
||||
.collect();
|
||||
let announcement = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::Custom(10044))
|
||||
.limit(1);
|
||||
|
||||
// Subscribe to the target
|
||||
client.subscribe(target).close_on(opts).with_id(id).await?;
|
||||
client
|
||||
.subscribe(vec![inbox, announcement])
|
||||
.close_on(opts)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -491,15 +484,9 @@ impl Room {
|
||||
|
||||
// Process each member
|
||||
for member in members {
|
||||
let relays = member.messaging_relays();
|
||||
let announcement = member.announcement();
|
||||
let public_key = member.public_key();
|
||||
|
||||
if relays.is_empty() {
|
||||
reports.push(SendReport::new(public_key).error("No messaging relays"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle encryption signer requirements
|
||||
if signer_kind.encryption() {
|
||||
if announcement.is_none() {
|
||||
@@ -535,8 +522,7 @@ impl Room {
|
||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
||||
};
|
||||
|
||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
||||
{
|
||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, public_key).await {
|
||||
Ok((report, _)) => {
|
||||
reports.push(report);
|
||||
sents += 1;
|
||||
@@ -549,12 +535,10 @@ impl Room {
|
||||
|
||||
// Send backup to current user if needed
|
||||
if backup && sents >= 1 {
|
||||
let relays = sender.messaging_relays();
|
||||
let public_key = sender.public_key();
|
||||
let signer = encryption_signer.as_ref().unwrap_or(&user_signer);
|
||||
|
||||
match send_gift_wrap(&client, signer, &public_key, &rumor, relays, public_key).await
|
||||
{
|
||||
match send_gift_wrap(&client, signer, &public_key, &rumor, public_key).await {
|
||||
Ok((report, _)) => reports.push(report),
|
||||
Err(report) => reports.push(report),
|
||||
}
|
||||
@@ -571,22 +555,16 @@ async fn send_gift_wrap<T>(
|
||||
signer: &T,
|
||||
receiver: &PublicKey,
|
||||
rumor: &UnsignedEvent,
|
||||
relays: &[RelayUrl],
|
||||
public_key: PublicKey,
|
||||
) -> Result<(SendReport, bool), SendReport>
|
||||
where
|
||||
T: NostrSigner + 'static,
|
||||
{
|
||||
// Ensure relay connections
|
||||
for url in relays {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
|
||||
Ok(event) => {
|
||||
match client
|
||||
.send_event(&event)
|
||||
.to(relays)
|
||||
.to_nip17()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -7,21 +7,11 @@ use settings::SignerKind;
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub enum Command {
|
||||
Insert(&'static str),
|
||||
ChangeSubject(&'static str),
|
||||
ChangeSubject(String),
|
||||
ChangeSigner(SignerKind),
|
||||
ToggleBackup,
|
||||
Subject,
|
||||
Copy(PublicKey),
|
||||
Relays(PublicKey),
|
||||
Njump(PublicKey),
|
||||
}
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub struct SeenOn(pub EventId);
|
||||
|
||||
/// Define a open public key action
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
||||
#[action(namespace = pubkey, no_json)]
|
||||
pub struct OpenPublicKey(pub PublicKey);
|
||||
|
||||
/// Define a copy inline public key action
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
||||
#[action(namespace = pubkey, no_json)]
|
||||
pub struct CopyPublicKey(pub PublicKey);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use actions::*;
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
@@ -24,10 +23,10 @@ use state::{NostrRegistry, upload};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::{ContextMenuExt, DropdownMenu};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{
|
||||
@@ -42,11 +41,6 @@ mod text;
|
||||
|
||||
const ANNOUNCEMENT: &str =
|
||||
"This conversation is private. Only members can see each other's messages.";
|
||||
const NO_INBOX: &str = "has not set up messaging relays. \
|
||||
They will not receive messages you send.";
|
||||
const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \
|
||||
You cannot send messages encrypted with an encryption key to them yet. \
|
||||
Coop automatically uses your identity to encrypt messages.";
|
||||
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
@@ -72,9 +66,15 @@ pub struct ChatPanel {
|
||||
/// Mapping message (rumor event) ids to their reports
|
||||
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
|
||||
|
||||
/// Input state
|
||||
/// Chat input state
|
||||
input: Entity<InputState>,
|
||||
|
||||
/// Subject input state
|
||||
subject_input: Entity<InputState>,
|
||||
|
||||
/// Subject bar visibility
|
||||
subject_bar: Entity<bool>,
|
||||
|
||||
/// Sent message ids
|
||||
sent_ids: Arc<RwLock<Vec<EventId>>>,
|
||||
|
||||
@@ -91,7 +91,7 @@ pub struct ChatPanel {
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Event subscriptions
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
subscriptions: SmallVec<[Subscription; 3]>,
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
@@ -124,19 +124,37 @@ impl ChatPanel {
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
// Define subject input state
|
||||
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
||||
let subject_bar = cx.new(|_cx| false);
|
||||
|
||||
// Define subscriptions
|
||||
let subscriptions =
|
||||
smallvec![
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe the chat input event
|
||||
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.send_text_message(window, cx);
|
||||
};
|
||||
})
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe the subject input event
|
||||
cx.subscribe_in(
|
||||
&subject_input,
|
||||
window,
|
||||
move |this, _input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.change_subject(window, cx);
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Define all functions that will run after the current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.connect(window, cx);
|
||||
this.handle_notifications(cx);
|
||||
this.subscribe_room_events(window, cx);
|
||||
this.get_messages(window, cx);
|
||||
@@ -149,6 +167,8 @@ impl ChatPanel {
|
||||
room,
|
||||
list_state,
|
||||
input,
|
||||
subject_input,
|
||||
subject_bar,
|
||||
replies_to,
|
||||
attachments,
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
@@ -160,49 +180,6 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all necessary data for each member
|
||||
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok((members, connect)) = self
|
||||
.room
|
||||
.read_with(cx, |this, cx| (this.members(), this.connect(cx)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Run the connect task in background
|
||||
self.tasks.push(connect);
|
||||
|
||||
// Spawn another task to verify after 3 seconds
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
|
||||
// Verify the connection
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
for member in members.into_iter() {
|
||||
let profile = persons.read(cx).get(&member, cx);
|
||||
|
||||
if profile.announcement().is_none() {
|
||||
let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
}
|
||||
|
||||
if profile.messaging_relays().is_empty() {
|
||||
let content = format!("{} {}", profile.name(), NO_INBOX);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Handle nostr notifications
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
@@ -254,10 +231,7 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(room) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(room) = self.room.upgrade() {
|
||||
self.subscriptions.push(
|
||||
// Subscribe to room events
|
||||
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
|
||||
@@ -272,6 +246,7 @@ impl ChatPanel {
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
fn get_messages(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -316,6 +291,16 @@ impl ChatPanel {
|
||||
content
|
||||
}
|
||||
|
||||
fn change_subject(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let subject = self.subject_input.read(cx).value();
|
||||
|
||||
self.room
|
||||
.update(cx, |this, cx| {
|
||||
this.set_subject(subject, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn send_text_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Get the message which includes all attachments
|
||||
let content = self.get_input_value(cx);
|
||||
@@ -505,10 +490,21 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_message(&self, id: &EventId, cx: &Context<Self>) {
|
||||
if let Some(message) = self.message(id) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.content.to_string()));
|
||||
fn copy_author(&self, public_key: &PublicKey, cx: &App) {
|
||||
let content = public_key.to_bech32().unwrap();
|
||||
let item = ClipboardItem::new_string(content);
|
||||
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
fn copy_message(&self, id: &EventId, cx: &App) {
|
||||
let Some(message) = self.message(id) else {
|
||||
return;
|
||||
};
|
||||
let content = message.content.to_string();
|
||||
let item = ClipboardItem::new_string(content);
|
||||
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) {
|
||||
@@ -558,7 +554,10 @@ impl ChatPanel {
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -588,7 +587,7 @@ impl ChatPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
|
||||
fn profile(&self, public_key: &PublicKey, cx: &App) -> Person {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
persons.read(cx).get(public_key, cx)
|
||||
}
|
||||
@@ -602,11 +601,14 @@ impl ChatPanel {
|
||||
if self
|
||||
.room
|
||||
.update(cx, |this, cx| {
|
||||
this.set_subject(*subject, cx);
|
||||
this.set_subject(subject, cx);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to change subject"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to change subject").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::ChangeSigner(kind) => {
|
||||
@@ -617,7 +619,10 @@ impl ChatPanel {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to change signer"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to change signer").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::ToggleBackup => {
|
||||
@@ -628,10 +633,101 @@ impl ChatPanel {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to toggle backup"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to toggle backup").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::Subject => {
|
||||
self.open_subject(window, cx);
|
||||
}
|
||||
Command::Copy(public_key) => {
|
||||
self.copy_author(public_key, cx);
|
||||
}
|
||||
Command::Relays(public_key) => {
|
||||
self.open_relays(public_key, window, cx);
|
||||
}
|
||||
Command::Njump(public_key) => {
|
||||
self.open_njump(public_key, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_subject(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let subject_input = self.subject_input.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let subject = subject_input.read(cx).value();
|
||||
|
||||
this.title("Change subject")
|
||||
.show_close(true)
|
||||
.confirm()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Subject:")),
|
||||
)
|
||||
.child(TextInput::new(&subject_input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::from(
|
||||
"Subject will be updated when you send a new message.",
|
||||
)),
|
||||
),
|
||||
)
|
||||
.on_ok(move |_ev, window, cx| {
|
||||
window
|
||||
.dispatch_action(Box::new(Command::ChangeSubject(subject.to_string())), cx);
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let profile = self.profile(public_key, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let relays = profile.messaging_relays();
|
||||
|
||||
this.title("Messaging Relays")
|
||||
.show_close(true)
|
||||
.child(v_flex().gap_1().children({
|
||||
let mut items = vec![];
|
||||
|
||||
for url in relays.iter() {
|
||||
items.push(
|
||||
h_flex()
|
||||
.h_7()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_sm()
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
.child(SharedString::from(url.to_string())),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fn open_njump(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
let content = format!("https://njump.me/{}", public_key.to_bech32().unwrap());
|
||||
cx.open_url(&content);
|
||||
}
|
||||
|
||||
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -758,18 +854,14 @@ impl ChatPanel {
|
||||
.flex()
|
||||
.gap_3()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{ix}-avatar")))
|
||||
.child(Avatar::new(author.avatar()))
|
||||
.context_menu(move |this, _window, _cx| {
|
||||
let view = Box::new(OpenPublicKey(public_key));
|
||||
let copy = Box::new(CopyPublicKey(public_key));
|
||||
|
||||
this.menu("View Profile", view)
|
||||
.menu("Copy Public Key", copy)
|
||||
}),
|
||||
)
|
||||
this.child(Avatar::new(author.avatar()).dropdown_menu(
|
||||
move |this, _window, _cx| {
|
||||
this.menu("Copy Public Key", Box::new(Command::Copy(public_key)))
|
||||
.menu("View Relays", Box::new(Command::Relays(public_key)))
|
||||
.separator()
|
||||
.menu("View on njump.me", Box::new(Command::Njump(public_key)))
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -807,8 +899,17 @@ impl ChatPanel {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(self.render_border(cx))
|
||||
.child(self.render_actions(&id, cx))
|
||||
.child(
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent),
|
||||
)
|
||||
.child(self.render_actions(&id, &public_key, cx))
|
||||
.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
@@ -896,11 +997,11 @@ impl ChatPanel {
|
||||
fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(SharedString::from(id.to_hex()))
|
||||
.gap_0p5()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_danger)
|
||||
.text_xs()
|
||||
.italic()
|
||||
.child(Icon::new(IconName::Info).xsmall())
|
||||
.child(Icon::new(IconName::Info).small())
|
||||
.child(SharedString::from(
|
||||
"Failed to send message. Click to see details.",
|
||||
))
|
||||
@@ -911,7 +1012,7 @@ impl ChatPanel {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.show_close(true)
|
||||
.title(SharedString::from("Sent Reports"))
|
||||
.child(v_flex().gap_4().pb_4().w_full().children({
|
||||
.child(v_flex().gap_4().w_full().children({
|
||||
let mut items = Vec::with_capacity(reports.len());
|
||||
|
||||
for report in reports.iter() {
|
||||
@@ -1030,18 +1131,12 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent)
|
||||
}
|
||||
|
||||
fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn render_actions(
|
||||
&self,
|
||||
id: &EventId,
|
||||
public_key: &PublicKey,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
@@ -1082,13 +1177,22 @@ impl ChatPanel {
|
||||
)
|
||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("seen-on")
|
||||
Button::new("advance")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu({
|
||||
let id = id.to_owned();
|
||||
move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id)))
|
||||
let public_key = *public_key;
|
||||
let _id = *id;
|
||||
move |this, _window, _cx| {
|
||||
this.menu("Copy author", Box::new(Command::Copy(public_key)))
|
||||
/*
|
||||
.menu(
|
||||
"Trace",
|
||||
Box::new(Command::Trace(id)),
|
||||
)
|
||||
*/
|
||||
}
|
||||
}),
|
||||
)
|
||||
.group_hover("", |this| this.visible())
|
||||
@@ -1286,12 +1390,30 @@ impl Panel for ChatPanel {
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Avatar::new(url).small())
|
||||
.child(Avatar::new(url).xsmall())
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or(div().child("Unknown").into_any_element())
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
let subject_bar = self.subject_bar.clone();
|
||||
|
||||
vec![
|
||||
Button::new("subject")
|
||||
.icon(IconName::Input)
|
||||
.tooltip("Change subject")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |_ev, _window, cx| {
|
||||
subject_bar.update(cx, |this, cx| {
|
||||
*this = !*this;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ChatPanel {}
|
||||
@@ -1307,6 +1429,33 @@ impl Render for ChatPanel {
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
.size_full()
|
||||
.when(*self.subject_bar.read(cx), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_12()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
TextInput::new(&self.subject_input)
|
||||
.text_sm()
|
||||
.small()
|
||||
.bordered(false),
|
||||
)
|
||||
.child(
|
||||
Button::new("change")
|
||||
.icon(IconName::CheckCircle)
|
||||
.label("Change")
|
||||
.secondary()
|
||||
.disabled(self.uploading)
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.change_subject(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{v_flex, Sizable};
|
||||
|
||||
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
||||
cx.new(|cx| Subject::new(subject, window, cx))
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
input: Entity<InputState>,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("Plan for holiday"));
|
||||
|
||||
if let Some(value) = subject {
|
||||
input.update(cx, |this, cx| {
|
||||
this.set_value(value, window, cx);
|
||||
});
|
||||
};
|
||||
|
||||
Self { input }
|
||||
}
|
||||
|
||||
pub fn new_subject(&self, cx: &App) -> SharedString {
|
||||
self.input.read(cx).value()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Subject:")),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::from(
|
||||
"Subject will be updated when you send a new message.",
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use chrono::{Local, TimeZone};
|
||||
use gpui::{Image, ImageFormat, SharedString};
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||
App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use state::{NostrRegistry, SignerEvent};
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::connect::ConnectSigner;
|
||||
use crate::dialogs::import::ImportKey;
|
||||
@@ -44,13 +44,14 @@ impl AccountSelector {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||
match event {
|
||||
SignerEvent::Set => {
|
||||
StateEvent::SignerSet => {
|
||||
window.close_all_modals(cx);
|
||||
window.refresh();
|
||||
}
|
||||
SignerEvent::Error(e) => {
|
||||
StateEvent::Error(e) => {
|
||||
this.set_error(e.to_string(), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -161,7 +162,7 @@ impl Render for AccountSelector {
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
||||
use common::TextUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Window,
|
||||
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Window, div, img, px,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use state::{
|
||||
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
||||
NOSTR_CONNECT_TIMEOUT,
|
||||
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
|
||||
StateEvent,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::v_flex;
|
||||
@@ -31,7 +31,7 @@ impl ConnectSigner {
|
||||
let error = cx.new(|_| None);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
@@ -55,7 +55,7 @@ impl ConnectSigner {
|
||||
|
||||
// Subscribe to the signer event
|
||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||
if let SignerEvent::Error(e) = event {
|
||||
if let StateEvent::Error(e) = event {
|
||||
this.set_error(e, cx);
|
||||
}
|
||||
});
|
||||
@@ -101,7 +101,7 @@ impl Render for ConnectSigner {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, Window,
|
||||
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, Window, div,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{v_flex, Disableable};
|
||||
use ui::{Disableable, v_flex};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImportKey {
|
||||
@@ -60,7 +60,7 @@ impl ImportKey {
|
||||
subscriptions.push(
|
||||
// Subscribe to the nostr signer event
|
||||
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||
if let SignerEvent::Error(e) = event {
|
||||
if let StateEvent::Error(e) = event {
|
||||
this.set_error(e, cx);
|
||||
}
|
||||
}),
|
||||
@@ -117,7 +117,7 @@ impl ImportKey {
|
||||
};
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
let timeout = Duration::from_secs(30);
|
||||
|
||||
// Construct the nostr connect signer
|
||||
@@ -293,7 +293,7 @@ impl Render for ImportKey {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::http_client::Url;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Window,
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use settings::{AppSettings, AuthMode};
|
||||
use theme::{ActiveTheme, ThemeMode};
|
||||
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::notification::Notification;
|
||||
use ui::switch::Switch;
|
||||
use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension};
|
||||
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
cx.new(|cx| Preferences::new(window, cx))
|
||||
@@ -41,7 +41,7 @@ impl Preferences {
|
||||
AppSettings::update_file_server(url, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(Notification::error(e.to_string()).autohide(false), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use assets::Assets;
|
||||
use gpui::{
|
||||
actions, point, px, size, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowOptions,
|
||||
App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
actions, point, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
use state::{APP_ID, CLIENT_NAME};
|
||||
@@ -86,7 +86,7 @@ fn main() {
|
||||
state::init(window, cx);
|
||||
|
||||
// Initialize person registry
|
||||
person::init(cx);
|
||||
person::init(window, cx);
|
||||
|
||||
// Initialize relay auth registry
|
||||
relay_auth::init(window, cx);
|
||||
|
||||
@@ -2,16 +2,16 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::KEYRING;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
|
||||
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
|
||||
|
||||
const MSG: &str = "Store your account keys in a safe location. \
|
||||
You can restore your account or move to another client anytime you want.";
|
||||
|
||||
@@ -4,20 +4,20 @@ use std::time::Duration;
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window,
|
||||
Task, TextAlign, Window, div, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
||||
cx.new(|cx| ContactListPanel::new(window, cx))
|
||||
@@ -156,15 +156,6 @@ impl ContactListPanel {
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
window.push_notification("Public Key not found", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
// Get contacts
|
||||
let contacts: Vec<Contact> = self
|
||||
@@ -177,14 +168,12 @@ impl ContactListPanel {
|
||||
self.set_updating(true, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct contact list event builder
|
||||
let builder = EventBuilder::contact_list(contacts);
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Set contact list
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -333,7 +322,7 @@ impl Render for ContactListPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use chat::{ChatRegistry, InboxState};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
||||
};
|
||||
use state::{NostrRegistry, RelayState};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||
|
||||
use crate::panels::{messaging_relays, profile, relay_list};
|
||||
use crate::panels::profile;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||
@@ -82,15 +79,6 @@ impl Render for GreeterPanel {
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let nip17 = chat.read(cx).state(cx);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let nip65 = nostr.read(cx).relay_list_state.clone();
|
||||
|
||||
let required_actions =
|
||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
@@ -130,64 +118,6 @@ impl Render for GreeterPanel {
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(required_actions, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Required Actions"))
|
||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.when(nip65.not_configured(), |this| {
|
||||
this.child(
|
||||
Button::new("relaylist")
|
||||
.icon(Icon::new(IconName::Relay))
|
||||
.label("Set up relay list")
|
||||
.ghost()
|
||||
.small()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
relay_list::init(window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(nip17.not_configured(), |this| {
|
||||
this.child(
|
||||
Button::new("import")
|
||||
.icon(Icon::new(IconName::Relay))
|
||||
.label("Set up messaging relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
messaging_relays::init(window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window,
|
||||
Task, TextAlign, Window, div, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||
|
||||
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
|
||||
Other users will find your relays and send messages to it.";
|
||||
@@ -170,15 +170,6 @@ impl MessagingRelayPanel {
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
window.push_notification("Public Key not found", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
// Construct event tags
|
||||
let tags: Vec<Tag> = self
|
||||
@@ -191,14 +182,12 @@ impl MessagingRelayPanel {
|
||||
self.set_updating(true, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct nip17 event builder
|
||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Set messaging relays
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -349,7 +338,7 @@ impl Render for MessagingRelayPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,21 +3,21 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||
Window,
|
||||
Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{shorten_pubkey, Person, PersonRegistry};
|
||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||
use settings::AppSettings;
|
||||
use state::{upload, NostrRegistry};
|
||||
use state::{NostrRegistry, upload};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
|
||||
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
|
||||
@@ -186,7 +186,10 @@ impl ProfilePanel {
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -269,7 +272,10 @@ impl ProfilePanel {
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, TextAlign, Window,
|
||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window, div, px, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||
|
||||
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
|
||||
where you will publish all your events. Others also publish events \
|
||||
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,16 +3,16 @@ use std::rc::Rc;
|
||||
use chat::RoomKind;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::dock_area::ClosePanel;
|
||||
use ui::dock::ClosePanel;
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex};
|
||||
|
||||
use crate::dialogs::screening;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry};
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
@@ -180,7 +180,10 @@ impl Sidebar {
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
};
|
||||
@@ -500,7 +503,7 @@ impl Render for Sidebar {
|
||||
.h(TABBAR_HEIGHT)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().tab_background)
|
||||
.child(
|
||||
TextInput::new(&self.find_input)
|
||||
.appearance(false)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::AppSettings;
|
||||
use chat::{ChatEvent, ChatRegistry, InboxState};
|
||||
use device::DeviceRegistry;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use device::{DeviceEvent, DeviceRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
@@ -11,17 +13,15 @@ use gpui::{
|
||||
use person::PersonRegistry;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{NostrRegistry, RelayState, SignerEvent};
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
||||
use title_bar::TitleBar;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::notification::Notification;
|
||||
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
use ui::notification::{Notification, NotificationKind};
|
||||
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::{accounts, settings};
|
||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||
@@ -37,6 +37,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
cx.new(|cx| Workspace::new(window, cx))
|
||||
}
|
||||
|
||||
struct RelayNotifcation;
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = workspace, no_json)]
|
||||
enum Command {
|
||||
@@ -63,15 +65,23 @@ pub struct Workspace {
|
||||
/// App's Dock Area
|
||||
dock: Entity<DockArea>,
|
||||
|
||||
/// Whether a user's relay list is connected
|
||||
relay_connected: bool,
|
||||
|
||||
/// Whether the inbox is connected
|
||||
inbox_connected: bool,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
_subscriptions: SmallVec<[Subscription; 6]>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let device = DeviceRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let npubs = nostr.read(cx).npubs();
|
||||
let chat = ChatRegistry::global(cx);
|
||||
|
||||
let titlebar = cx.new(|_| TitleBar::new());
|
||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||
|
||||
@@ -96,9 +106,56 @@ impl Workspace {
|
||||
subscriptions.push(
|
||||
// Subscribe to the signer events
|
||||
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||
if let SignerEvent::Set = event {
|
||||
this.set_center_layout(window, cx);
|
||||
match event {
|
||||
StateEvent::Connecting => {
|
||||
let note = Notification::new()
|
||||
.id::<RelayNotifcation>()
|
||||
.message("Connecting to the bootstrap relay...")
|
||||
.with_kind(NotificationKind::Info)
|
||||
.icon(IconName::Relay);
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
StateEvent::Connected => {
|
||||
let note = Notification::new()
|
||||
.id::<RelayNotifcation>()
|
||||
.message("Connected to the bootstrap relay")
|
||||
.with_kind(NotificationKind::Success)
|
||||
.icon(IconName::Relay);
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
StateEvent::RelayNotConfigured => {
|
||||
this.relay_notification(window, cx);
|
||||
}
|
||||
StateEvent::RelayConnected => {
|
||||
window.clear_notification::<RelayNotifcation>(cx);
|
||||
this.set_relay_connected(true, cx);
|
||||
}
|
||||
StateEvent::SignerSet => {
|
||||
this.set_center_layout(window, cx);
|
||||
this.set_relay_connected(false, cx);
|
||||
this.set_inbox_connected(false, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe all events emitted by the device registry
|
||||
cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| {
|
||||
match ev {
|
||||
DeviceEvent::Set => {
|
||||
window.push_notification(
|
||||
Notification::success("Encryption Key has been set"),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
DeviceEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -130,6 +187,12 @@ impl Workspace {
|
||||
});
|
||||
});
|
||||
}
|
||||
ChatEvent::Subscribed => {
|
||||
this.set_inbox_connected(true, cx);
|
||||
}
|
||||
ChatEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
@@ -154,6 +217,8 @@ impl Workspace {
|
||||
Self {
|
||||
titlebar,
|
||||
dock,
|
||||
relay_connected: false,
|
||||
inbox_connected: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -185,6 +250,18 @@ impl Workspace {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Set whether the relay list is connected
|
||||
fn set_relay_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
||||
self.relay_connected = connected;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set whether the inbox is connected
|
||||
fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
||||
self.inbox_connected = connected;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the dock layout
|
||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||
@@ -198,8 +275,8 @@ impl Workspace {
|
||||
/// Set the center dock layout
|
||||
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let dock = self.dock.downgrade();
|
||||
let greeeter = Arc::new(greeter::init(window, cx));
|
||||
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
|
||||
let greeter = Arc::new(greeter::init(window, cx));
|
||||
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
|
||||
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
||||
|
||||
// Update the layout with center dock
|
||||
@@ -267,6 +344,12 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::RefreshMessagingRelays => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |this, cx| {
|
||||
this.get_messages(cx);
|
||||
});
|
||||
}
|
||||
Command::ShowRelayList => {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
@@ -277,27 +360,25 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::RefreshRelayList => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
if let Some(public_key) = signer.public_key() {
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
Command::RefreshEncryption => {
|
||||
let device = DeviceRegistry::global(cx);
|
||||
device.update(cx, |this, cx| {
|
||||
this.get_announcement(cx);
|
||||
});
|
||||
}
|
||||
Command::RefreshRelayList => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(cx);
|
||||
});
|
||||
}
|
||||
Command::ResetEncryption => {
|
||||
self.confirm_reset_encryption(window, cx);
|
||||
}
|
||||
Command::RefreshMessagingRelays => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |this, cx| {
|
||||
this.ensure_messaging_relays(cx);
|
||||
});
|
||||
}
|
||||
Command::ToggleTheme => {
|
||||
self.theme_selector(window, cx);
|
||||
}
|
||||
@@ -341,8 +422,10 @@ impl Workspace {
|
||||
window.close_modal(cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window
|
||||
.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -450,7 +533,56 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
|
||||
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn relay_notification(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const BODY: &str = "Coop cannot found your gossip relay list. \
|
||||
Maybe you haven't set it yet or relay not responsed";
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity = nostr.downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
|
||||
let note = Notification::new()
|
||||
.autohide(false)
|
||||
.id::<RelayNotifcation>()
|
||||
.icon(IconName::Relay)
|
||||
.title("Gossip Relays are required")
|
||||
.message(BODY)
|
||||
.action(move |_this, _window, _cx| {
|
||||
let entity = entity.clone();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Button::new("retry")
|
||||
.label("Retry")
|
||||
.small()
|
||||
.primary()
|
||||
.loading(loading.get())
|
||||
.disabled(loading.get())
|
||||
.on_click({
|
||||
let loading = Rc::clone(&loading);
|
||||
|
||||
move |_ev, _window, cx| {
|
||||
// Set loading state to true
|
||||
loading.set(true);
|
||||
// Retry
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
|
||||
fn titlebar_left(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
let current_user = signer.public_key();
|
||||
@@ -529,14 +661,14 @@ impl Workspace {
|
||||
})
|
||||
}
|
||||
|
||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let relay_connected = self.relay_connected;
|
||||
let inbox_connected = self.inbox_connected;
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let inbox_state = chat.read(cx).state(cx);
|
||||
|
||||
let Some(pkey) = signer.public_key() else {
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return div();
|
||||
};
|
||||
|
||||
@@ -554,7 +686,7 @@ impl Workspace {
|
||||
let state = device.read(cx).state();
|
||||
|
||||
this.min_w(px(260.))
|
||||
.item(PopupMenuItem::element(move |_window, _cx| {
|
||||
.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.w_full()
|
||||
@@ -566,7 +698,7 @@ impl Workspace {
|
||||
.rounded_full()
|
||||
.when(state.set(), |this| this.bg(gpui::green()))
|
||||
.when(state.requesting(), |this| {
|
||||
this.bg(gpui::yellow())
|
||||
this.bg(cx.theme().icon_accent)
|
||||
}),
|
||||
)
|
||||
.child(SharedString::from(state.to_string()))
|
||||
@@ -584,37 +716,21 @@ impl Workspace {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.map(|this| match inbox_state {
|
||||
InboxState::Checking => this.child(div().child(
|
||||
SharedString::from("Fetching user's messaging relay list..."),
|
||||
)),
|
||||
InboxState::RelayNotAvailable => {
|
||||
this.child(div().text_color(cx.theme().warning_active).child(
|
||||
SharedString::from(
|
||||
"User hasn't configured a messaging relay list",
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => this,
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("inbox")
|
||||
.icon(IconName::Inbox)
|
||||
.tooltip("Inbox")
|
||||
.small()
|
||||
.ghost()
|
||||
.when(inbox_state.subscribing(), |this| this.indicator())
|
||||
.loading(!inbox_connected)
|
||||
.disabled(!inbox_connected)
|
||||
.when(!inbox_connected, |this| {
|
||||
this.tooltip("Connecting to user's messaging relays...")
|
||||
})
|
||||
.when(inbox_connected, |this| this.indicator())
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&pkey, cx);
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
|
||||
let urls: Vec<SharedString> = profile
|
||||
.messaging_relays()
|
||||
.iter()
|
||||
@@ -632,9 +748,7 @@ impl Workspace {
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||
)
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
.child(url.clone())
|
||||
}))
|
||||
});
|
||||
@@ -652,73 +766,32 @@ impl Workspace {
|
||||
Box::new(Command::ShowMessaging),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.map(|this| match nostr.read(cx).relay_list_state {
|
||||
RelayState::Checking => this
|
||||
.child(div().child(SharedString::from(
|
||||
"Fetching user's relay list...",
|
||||
))),
|
||||
RelayState::NotConfigured => {
|
||||
this.child(div().text_color(cx.theme().warning_active).child(
|
||||
SharedString::from("User hasn't configured a relay list"),
|
||||
))
|
||||
}
|
||||
_ => this,
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("relay-list")
|
||||
.icon(IconName::Relay)
|
||||
.tooltip("User's relay list")
|
||||
.small()
|
||||
.ghost()
|
||||
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
||||
this.indicator()
|
||||
.loading(!relay_connected)
|
||||
.disabled(!relay_connected)
|
||||
.when(!relay_connected, |this| {
|
||||
this.tooltip("Connecting to user's relay list...")
|
||||
})
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
||||
|
||||
// Header
|
||||
let menu = this.min_w(px(260.)).label("Relays");
|
||||
|
||||
// Content
|
||||
let menu = urls.into_iter().fold(menu, |this, url| {
|
||||
this.item(PopupMenuItem::element(move |_window, _cx| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||
)
|
||||
.child(url.clone())
|
||||
}))
|
||||
});
|
||||
|
||||
// Footer
|
||||
menu.separator()
|
||||
.when(relay_connected, |this| this.indicator())
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.label("User's Relay List")
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::RefreshRelayList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Update relay list",
|
||||
"Update",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowRelayList),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,19 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
|
||||
SharedString, Styled, Task, Window, div, relative,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{
|
||||
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
|
||||
};
|
||||
use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::notification::Notification;
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const IDENTIFIER: &str = "coop:device";
|
||||
const MSG: &str = "You've requested an encryption key from another device. \
|
||||
@@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity<DeviceRegistry>);
|
||||
|
||||
impl Global for GlobalDeviceRegistry {}
|
||||
|
||||
/// Device event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum DeviceEvent {
|
||||
/// A new encryption signer has been set
|
||||
Set,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Device Registry
|
||||
///
|
||||
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
@@ -42,11 +48,10 @@ pub struct DeviceRegistry {
|
||||
|
||||
/// Async tasks
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DeviceEvent> for DeviceRegistry {}
|
||||
|
||||
impl DeviceRegistry {
|
||||
/// Retrieve the global device registry state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
@@ -60,27 +65,16 @@ impl DeviceRegistry {
|
||||
|
||||
/// Create a new device registry instance
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
let state = DeviceState::default();
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the NIP-65 state
|
||||
cx.observe(&nostr, |this, state, cx| {
|
||||
if state.read(cx).relay_list_state == RelayState::Configured {
|
||||
this.get_announcement(cx);
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.handle_notifications(window, cx);
|
||||
this.get_announcement(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
state: DeviceState::default(),
|
||||
state,
|
||||
tasks: vec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +117,7 @@ impl DeviceRegistry {
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(
|
||||
// Update GPUI states
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
while let Ok(event) = rx.recv_async().await {
|
||||
match event.kind {
|
||||
// New request event
|
||||
@@ -145,8 +137,7 @@ impl DeviceRegistry {
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get the device state
|
||||
@@ -191,45 +182,68 @@ impl DeviceRegistry {
|
||||
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.subscribe_to_giftwrap_events(cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
task.await?;
|
||||
|
||||
// Update state
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
||||
/// Get the messaging relays for the current user
|
||||
fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
cx.background_spawn(async move {
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
let relay_urls = profile.messaging_relays().clone();
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
// Extract relay URLs from the event
|
||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||
|
||||
// Ensure all relays are connected
|
||||
for url in urls.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Err(anyhow!("Relays not found"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
let urls = self.get_user_messaging_relays(cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = urls.await?;
|
||||
let encryption = signer.get_encryption_signer().await.context("not found")?;
|
||||
let public_key = encryption.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<RelayUrl, Filter> = relay_urls
|
||||
let target: HashMap<RelayUrl, Filter> = urls
|
||||
.into_iter()
|
||||
.map(|relay| (relay, filter.clone()))
|
||||
.collect();
|
||||
|
||||
let output = client.subscribe(target).with_id(id).await?;
|
||||
|
||||
log::info!(
|
||||
"Successfully subscribed to encryption gift-wrap messages on: {:?}",
|
||||
output.success
|
||||
);
|
||||
// Subscribe
|
||||
client.subscribe(target).with_id(id).await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -239,20 +253,13 @@ impl DeviceRegistry {
|
||||
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Reset state before fetching announcement
|
||||
self.reset(cx);
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// Construct the filter for the device announcement event
|
||||
let filter = Filter::new()
|
||||
@@ -260,29 +267,19 @@ impl DeviceRegistry {
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Stream events from user's write relays
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
log::info!("Received device announcement event: {event:?}");
|
||||
if let Ok(event) = res {
|
||||
return Ok(event);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive device announcement event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Device announcement not found"))
|
||||
Err(anyhow!("Announcement not found"))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
@@ -307,22 +304,12 @@ impl DeviceRegistry {
|
||||
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let keys = Keys::generate();
|
||||
let secret = keys.secret_key().to_secret_hex();
|
||||
let n = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct an announcement event
|
||||
let event = client
|
||||
.sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![
|
||||
@@ -332,7 +319,7 @@ impl DeviceRegistry {
|
||||
.await?;
|
||||
|
||||
// Publish announcement
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
// Save device keys to the database
|
||||
set_keys(&client, &secret).await?;
|
||||
@@ -409,23 +396,15 @@ impl DeviceRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct a filter for device key requests
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4454))
|
||||
.author(public_key)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe to the device key requests on user's write relays
|
||||
client.subscribe(target).await?;
|
||||
client.subscribe(filter).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -443,23 +422,15 @@ impl DeviceRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct a filter for device key requests
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4455))
|
||||
.author(public_key)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe to the device key requests on user's write relays
|
||||
client.subscribe(target).await?;
|
||||
client.subscribe(filter).await?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
@@ -471,13 +442,7 @@ impl DeviceRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
let app_pubkey = app_keys.public_key();
|
||||
|
||||
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
||||
@@ -507,8 +472,6 @@ impl DeviceRegistry {
|
||||
Ok(Some(keys))
|
||||
}
|
||||
None => {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct an event for device key request
|
||||
let event = client
|
||||
.sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![
|
||||
@@ -518,7 +481,7 @@ impl DeviceRegistry {
|
||||
.await?;
|
||||
|
||||
// Send the event to write relays
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
@@ -549,7 +512,7 @@ impl DeviceRegistry {
|
||||
/// Parse the response event for device keys from other devices
|
||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
|
||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||
let root_device = event
|
||||
@@ -586,18 +549,11 @@ impl DeviceRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
let event = event.clone();
|
||||
let id: SharedString = event.id.to_hex().into();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Get device keys
|
||||
let keys = get_keys(&client).await?;
|
||||
let secret = keys.secret_key().to_secret_hex();
|
||||
@@ -626,7 +582,7 @@ impl DeviceRegistry {
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Send the response event to the user's relay list
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -635,13 +591,16 @@ impl DeviceRegistry {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.clear_notification(id, cx);
|
||||
window.clear_notification_by_id::<DeviceNotification>(id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -671,17 +630,23 @@ impl DeviceRegistry {
|
||||
|
||||
let entity = cx.entity().downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
let key = SharedString::from(event.id.to_hex());
|
||||
|
||||
Notification::new()
|
||||
.custom_id(SharedString::from(event.id.to_hex()))
|
||||
.type_id::<DeviceNotification>(key)
|
||||
.autohide(false)
|
||||
.icon(IconName::UserKey)
|
||||
.title(SharedString::from("New request"))
|
||||
.content(move |_window, cx| {
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(SharedString::from(MSG))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.line_height(relative(1.25))
|
||||
.child(SharedString::from(MSG)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -733,7 +698,7 @@ impl DeviceRegistry {
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.action(move |_window, _cx| {
|
||||
.action(move |_this, _window, _cx| {
|
||||
let view = entity.clone();
|
||||
let event = event.clone();
|
||||
|
||||
@@ -759,6 +724,8 @@ impl DeviceRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceNotification;
|
||||
|
||||
/// Verify the author of an event
|
||||
async fn verify_author(client: &Client, event: &Event) -> bool {
|
||||
if let Some(signer) = client.signer() {
|
||||
|
||||
@@ -15,3 +15,4 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
flume.workspace = true
|
||||
log.workspace = true
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -3,19 +3,19 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::EventUtils;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||
|
||||
mod person;
|
||||
|
||||
pub use person::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
||||
pub fn init(window: &mut Window, cx: &mut App) {
|
||||
PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
|
||||
}
|
||||
|
||||
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||
@@ -36,13 +36,13 @@ pub struct PersonRegistry {
|
||||
persons: HashMap<PublicKey, Entity<Person>>,
|
||||
|
||||
/// Set of public keys that have been seen
|
||||
seen: Rc<RefCell<HashSet<PublicKey>>>,
|
||||
seens: Rc<RefCell<HashSet<PublicKey>>>,
|
||||
|
||||
/// Sender for requesting metadata
|
||||
sender: flume::Sender<PublicKey>,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
_tasks: SmallVec<[Task<()>; 4]>,
|
||||
tasks: SmallVec<[Task<()>; 4]>,
|
||||
}
|
||||
|
||||
impl PersonRegistry {
|
||||
@@ -57,13 +57,13 @@ impl PersonRegistry {
|
||||
}
|
||||
|
||||
/// Create a new person registry instance
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
|
||||
let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
|
||||
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
@@ -111,33 +111,16 @@ impl PersonRegistry {
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Load all user profiles from the database
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = cx
|
||||
.background_executor()
|
||||
.await_on_background(async move { load_persons(&client).await })
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(persons) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.bulk_inserts(persons, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load all persons from the database: {e}");
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
cx.defer_in(window, |this, _window, cx| {
|
||||
this.load(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
persons: HashMap::new(),
|
||||
seen: Rc::new(RefCell::new(HashSet::new())),
|
||||
seens: Rc::new(RefCell::new(HashSet::new())),
|
||||
sender: mta_tx,
|
||||
_tasks: tasks,
|
||||
tasks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,25 +146,21 @@ impl PersonRegistry {
|
||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||
let person = Person::new(event.pubkey, metadata);
|
||||
let val = Box::new(person);
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Person(val)).await.ok();
|
||||
}
|
||||
Kind::ContactList => {
|
||||
let public_keys = event.extract_public_keys();
|
||||
|
||||
// Get metadata for all public keys
|
||||
get_metadata(client, public_keys).await.ok();
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
let val = Box::new(event.into_owned());
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Relays(val)).await.ok();
|
||||
}
|
||||
Kind::Custom(10044) => {
|
||||
let val = Box::new(event.into_owned());
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
||||
}
|
||||
@@ -198,7 +177,7 @@ impl PersonRegistry {
|
||||
loop {
|
||||
match flume::Selector::new()
|
||||
.recv(rx, |result| result.ok())
|
||||
.wait_timeout(Duration::from_secs(2))
|
||||
.wait_timeout(Duration::from_secs(TIMEOUT))
|
||||
{
|
||||
Ok(Some(public_key)) => {
|
||||
batch.insert(public_key);
|
||||
@@ -208,40 +187,81 @@ impl PersonRegistry {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !batch.is_empty() {
|
||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all user profiles from the database
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let task: Task<Result<Vec<Person>, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
let persons = events
|
||||
.into_iter()
|
||||
.map(|event| {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
Person::new(event.pubkey, metadata)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(persons)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Ok(persons) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.bulk_inserts(persons, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
let announcement = Announcement::from(event);
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_announcement(announcement);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person =
|
||||
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set messaging relays for a person
|
||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_messaging_relays(urls);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert batch of persons
|
||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||
for person in persons.into_iter() {
|
||||
self.persons.insert(person.public_key(), cx.new(|_| person));
|
||||
let public_key = person.public_key();
|
||||
self.persons
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| cx.new(|_| person));
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -270,7 +290,7 @@ impl PersonRegistry {
|
||||
}
|
||||
|
||||
let public_key = *public_key;
|
||||
let mut seen = self.seen.borrow_mut();
|
||||
let mut seen = self.seens.borrow_mut();
|
||||
|
||||
if seen.insert(public_key) {
|
||||
let sender = self.sender.clone();
|
||||
@@ -322,19 +342,3 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load all user profiles from the database
|
||||
async fn load_persons(client: &Client) -> Result<Vec<Person>, Error> {
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
|
||||
let mut persons = vec![];
|
||||
|
||||
for event in events.into_iter() {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
let person = Person::new(event.pubkey, metadata);
|
||||
persons.push(person);
|
||||
}
|
||||
|
||||
Ok(persons)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,21 @@ impl Person {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build profile encryption keys announcement
|
||||
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
|
||||
self.announcement = Some(announcement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build profile messaging relays
|
||||
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get profile public key
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.public_key
|
||||
@@ -75,21 +90,11 @@ impl Person {
|
||||
self.metadata.clone()
|
||||
}
|
||||
|
||||
/// Set profile metadata
|
||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||
self.metadata = metadata;
|
||||
}
|
||||
|
||||
/// Get profile encryption keys announcement
|
||||
pub fn announcement(&self) -> Option<Announcement> {
|
||||
self.announcement.clone()
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||
self.announcement = Some(announcement);
|
||||
}
|
||||
|
||||
/// Get profile messaging relays
|
||||
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
||||
&self.messaging_relays
|
||||
@@ -100,14 +105,6 @@ impl Person {
|
||||
self.messaging_relays.first().cloned()
|
||||
}
|
||||
|
||||
/// Set profile messaging relays
|
||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
}
|
||||
|
||||
/// Get profile avatar
|
||||
pub fn avatar(&self) -> SharedString {
|
||||
self.metadata()
|
||||
@@ -115,8 +112,9 @@ impl Person {
|
||||
.as_ref()
|
||||
.filter(|picture| !picture.is_empty())
|
||||
.map(|picture| {
|
||||
let encoded_picture = urlencoding::encode(picture);
|
||||
let url = format!(
|
||||
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||
);
|
||||
url.into()
|
||||
})
|
||||
@@ -139,6 +137,24 @@ impl Person {
|
||||
|
||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
||||
}
|
||||
|
||||
/// Set profile metadata
|
||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||
self.metadata = metadata;
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||
self.announcement = Some(announcement);
|
||||
}
|
||||
|
||||
/// Set profile messaging relays
|
||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
||||
@@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
||||
let Ok(pubkey) = public_key.to_bech32();
|
||||
|
||||
format!(
|
||||
"{}:{}",
|
||||
"{}...{}",
|
||||
&pubkey[0..(len + 1)],
|
||||
&pubkey[pubkey.len() - len..]
|
||||
)
|
||||
|
||||
@@ -5,19 +5,19 @@ use std::hash::Hash;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
||||
Task, Window,
|
||||
Task, Window, div, relative,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::{AppSettings, AuthMode};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::notification::Notification;
|
||||
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
|
||||
|
||||
const AUTH_MESSAGE: &str =
|
||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
||||
@@ -34,7 +34,10 @@ struct AuthRequest {
|
||||
}
|
||||
|
||||
impl AuthRequest {
|
||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
||||
pub fn new<S>(challenge: S, url: RelayUrl) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self {
|
||||
challenge: challenge.into(),
|
||||
url,
|
||||
@@ -106,22 +109,6 @@ impl RelayAuth {
|
||||
tx.send_async(signal).await.ok();
|
||||
}
|
||||
}
|
||||
RelayMessage::Closed {
|
||||
subscription_id,
|
||||
message,
|
||||
} => {
|
||||
let msg = MachineReadablePrefix::parse(&message);
|
||||
|
||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||
if let Ok(Some(relay)) = client.relay(&relay_url).await {
|
||||
// Send close message to relay
|
||||
relay
|
||||
.send_msg(ClientMessage::Close(subscription_id))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
RelayMessage::Ok {
|
||||
event_id, message, ..
|
||||
} => {
|
||||
@@ -273,7 +260,7 @@ impl RelayAuth {
|
||||
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
||||
let settings = AppSettings::global(cx);
|
||||
let req = req.clone();
|
||||
let challenge = req.challenge().to_string();
|
||||
let challenge = SharedString::from(req.challenge().to_string());
|
||||
|
||||
// Create a task for authentication
|
||||
let task = self.auth(&req, cx);
|
||||
@@ -283,7 +270,7 @@ impl RelayAuth {
|
||||
let url = req.url();
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.clear_notification(challenge, cx);
|
||||
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
@@ -295,10 +282,19 @@ impl RelayAuth {
|
||||
this.add_trusted_relay(url, cx);
|
||||
});
|
||||
|
||||
window.push_notification(format!("{} has been authenticated", url), cx);
|
||||
window.push_notification(
|
||||
Notification::success(format!(
|
||||
"Relay {} has been authenticated",
|
||||
url.domain().unwrap_or_default()
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -323,20 +319,25 @@ impl RelayAuth {
|
||||
/// Build a notification for the authentication request.
|
||||
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
||||
let req = req.clone();
|
||||
let challenge = SharedString::from(req.challenge.clone());
|
||||
let url = SharedString::from(req.url().to_string());
|
||||
let entity = cx.entity().downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
|
||||
Notification::new()
|
||||
.custom_id(SharedString::from(&req.challenge))
|
||||
.type_id::<AuthNotification>(challenge)
|
||||
.autohide(false)
|
||||
.icon(IconName::Info)
|
||||
.icon(IconName::Warning)
|
||||
.title(SharedString::from("Authentication Required"))
|
||||
.content(move |_window, cx| {
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(SharedString::from(AUTH_MESSAGE))
|
||||
.line_height(relative(1.25))
|
||||
.child(SharedString::from(AUTH_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
@@ -349,7 +350,7 @@ impl RelayAuth {
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.action(move |_window, _cx| {
|
||||
.action(move |_this, _window, _cx| {
|
||||
let view = entity.clone();
|
||||
let req = req.clone();
|
||||
|
||||
@@ -374,3 +375,5 @@ impl RelayAuth {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthNotification;
|
||||
|
||||
@@ -10,6 +10,8 @@ common = { path = "../common" }
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
nostr-lmdb.workspace = true
|
||||
nostr-memory.workspace = true
|
||||
nostr-gossip-sqlite.workspace = true
|
||||
nostr-connect.workspace = true
|
||||
nostr-blossom.workspace = true
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||
|
||||
/// Default bootstrap relays
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.primal.net",
|
||||
"wss://indexer.coracle.social",
|
||||
"wss://user.kindpag.es",
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use gpui::SharedString;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
/// Gossip
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Gossip {
|
||||
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
|
||||
}
|
||||
|
||||
impl Gossip {
|
||||
pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec<SharedString> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.map(|(url, _)| url.to_string().into())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get read relays for a given public key
|
||||
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
|
||||
Some(url.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get write relays for a given public key
|
||||
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Insert gossip relays for a public key
|
||||
pub fn insert_relays(&mut self, event: &Event) {
|
||||
self.relays.entry(event.pubkey).or_default().extend(
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::RelayMetadata {
|
||||
relay_url,
|
||||
metadata,
|
||||
}) = tag.clone().to_standardized()
|
||||
{
|
||||
Some((relay_url, metadata))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.take(3),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -6,20 +6,20 @@ use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use common::config_dir;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_gossip_sqlite::prelude::*;
|
||||
use nostr_lmdb::prelude::*;
|
||||
use nostr_memory::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
mod blossom;
|
||||
mod constants;
|
||||
mod device;
|
||||
mod gossip;
|
||||
mod nip05;
|
||||
mod signer;
|
||||
|
||||
pub use blossom::*;
|
||||
pub use constants::*;
|
||||
pub use device::*;
|
||||
pub use gossip::*;
|
||||
pub use nip05::*;
|
||||
pub use signer::*;
|
||||
|
||||
@@ -41,6 +41,23 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||
|
||||
impl Global for GlobalNostrRegistry {}
|
||||
|
||||
/// Signer event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum StateEvent {
|
||||
/// Connecting to the bootstrapping relay
|
||||
Connecting,
|
||||
/// Connected to the bootstrapping relay
|
||||
Connected,
|
||||
/// User has not set up NIP-65 relays
|
||||
RelayNotConfigured,
|
||||
/// Connected to NIP-65 relays
|
||||
RelayConnected,
|
||||
/// A new signer has been set
|
||||
SignerSet,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Nostr Registry
|
||||
#[derive(Debug)]
|
||||
pub struct NostrRegistry {
|
||||
@@ -53,21 +70,17 @@ pub struct NostrRegistry {
|
||||
/// Local public keys
|
||||
npubs: Entity<Vec<PublicKey>>,
|
||||
|
||||
/// Custom gossip implementation
|
||||
gossip: Entity<Gossip>,
|
||||
|
||||
/// App keys
|
||||
///
|
||||
/// Used for Nostr Connect and NIP-4e operations
|
||||
pub app_keys: Keys,
|
||||
|
||||
/// Relay list state
|
||||
pub relay_list_state: RelayState,
|
||||
app_keys: Keys,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<StateEvent> for NostrRegistry {}
|
||||
|
||||
impl NostrRegistry {
|
||||
/// Retrieve the global nostr state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
@@ -88,32 +101,43 @@ impl NostrRegistry {
|
||||
// Construct the nostr npubs entity
|
||||
let npubs = cx.new(|_| vec![]);
|
||||
|
||||
// Construct the gossip entity
|
||||
let gossip = cx.new(|_| Gossip::default());
|
||||
// Construct the nostr gossip instance
|
||||
let gossip = cx.foreground_executor().block_on(async move {
|
||||
NostrGossipSqlite::open(config_dir().join("gossip"))
|
||||
.await
|
||||
.expect("Failed to initialize gossip instance")
|
||||
});
|
||||
|
||||
// Construct the nostr client builder
|
||||
let mut builder = ClientBuilder::default()
|
||||
.signer(signer.clone())
|
||||
.gossip(gossip)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
});
|
||||
|
||||
// Add database if not in debug mode
|
||||
if !cfg!(debug_assertions) {
|
||||
// Construct the nostr lmdb instance
|
||||
let lmdb = cx.foreground_executor().block_on(async move {
|
||||
NostrLmdb::open(config_dir().join("nostr"))
|
||||
.await
|
||||
.expect("Failed to initialize database")
|
||||
});
|
||||
builder = builder.database(lmdb);
|
||||
} else {
|
||||
builder = builder.database(MemoryDatabase::unbounded())
|
||||
}
|
||||
|
||||
// Construct the nostr client
|
||||
let client = ClientBuilder::default()
|
||||
.signer(signer.clone())
|
||||
.database(lmdb)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
})
|
||||
.build();
|
||||
// Build the nostr client
|
||||
let client = builder.build();
|
||||
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, _window, cx| {
|
||||
this.connect(cx);
|
||||
this.handle_notifications(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -121,8 +145,6 @@ impl NostrRegistry {
|
||||
signer,
|
||||
npubs,
|
||||
app_keys,
|
||||
gossip,
|
||||
relay_list_state: RelayState::Idle,
|
||||
tasks: vec![],
|
||||
}
|
||||
}
|
||||
@@ -142,94 +164,57 @@ impl NostrRegistry {
|
||||
self.npubs.clone()
|
||||
}
|
||||
|
||||
/// Get the app keys
|
||||
pub fn keys(&self) -> Keys {
|
||||
self.app_keys.clone()
|
||||
}
|
||||
|
||||
/// Connect to the bootstrapping relays
|
||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.await_on_background(async move {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
// Add search relay to the relay pool
|
||||
for url in SEARCH_RELAYS.into_iter() {
|
||||
client.add_relay(url).await.ok();
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Add bootstrap relay to the relay pool
|
||||
for url in BOOTSTRAP_RELAYS.into_iter() {
|
||||
client.add_relay(url).await.ok();
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Connect to all added relays
|
||||
client.connect().and_wait(Duration::from_secs(2)).await;
|
||||
})
|
||||
.await;
|
||||
client.connect().await;
|
||||
|
||||
// Update the state
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Emit connecting event
|
||||
cx.emit(StateEvent::Connecting);
|
||||
|
||||
self.tasks
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.emit(StateEvent::Connected);
|
||||
this.get_npubs(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Handle nostr notifications
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let gossip = self.gossip.downgrade();
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Event>(2048);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
// Handle nostr notifications
|
||||
let mut notifications = client.notifications();
|
||||
let mut processed_events = HashSet::new();
|
||||
|
||||
while let Some(notification) = notifications.next().await {
|
||||
if let ClientNotification::Message {
|
||||
message:
|
||||
RelayMessage::Event {
|
||||
event,
|
||||
subscription_id,
|
||||
},
|
||||
..
|
||||
} = notification
|
||||
{
|
||||
if !processed_events.insert(event.id) {
|
||||
// Skip if the event has already been processed
|
||||
continue;
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Kind::RelayList = event.kind {
|
||||
if subscription_id.as_str().contains("room-") {
|
||||
get_events_for_room(&client, &event).await.ok();
|
||||
}
|
||||
tx.send_async(event.into_owned()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
while let Ok(event) = rx.recv_async().await {
|
||||
if let Kind::RelayList = event.kind {
|
||||
gossip.update(cx, |this, cx| {
|
||||
this.insert_relays(&event);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get all used npubs
|
||||
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
||||
let npubs = self.npubs.downgrade();
|
||||
|
||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||
let dir = config_dir().join("keys");
|
||||
// Ensure keys directory exists
|
||||
@@ -269,25 +254,26 @@ impl NostrRegistry {
|
||||
true => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.create_identity(cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
false => {
|
||||
// TODO: auto login
|
||||
npubs.update(cx, |this, cx| {
|
||||
npubs
|
||||
.update(cx, |this, cx| {
|
||||
this.extend(public_keys);
|
||||
cx.notify();
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to get npubs: {e}");
|
||||
this.update(cx, |this, cx| {
|
||||
this.create_identity(cx);
|
||||
})?;
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -307,74 +293,49 @@ impl NostrRegistry {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let signer = async_keys.into_nostr_signer();
|
||||
|
||||
// Get default relay list
|
||||
// Construct relay list event
|
||||
let relay_list = default_relay_list();
|
||||
|
||||
// Extract write relays
|
||||
let write_urls: Vec<RelayUrl> = relay_list
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Ensure connected to all relays
|
||||
for (url, _metadata) in relay_list.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
|
||||
// Publish relay list event
|
||||
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
|
||||
let output = client
|
||||
|
||||
// Publish relay list
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(BOOTSTRAP_RELAYS)
|
||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
log::info!("Sent gossip relay list: {output:?}");
|
||||
|
||||
// Construct the default metadata
|
||||
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
|
||||
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
|
||||
let metadata = Metadata::new().display_name(&name).picture(avatar);
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
|
||||
// Publish metadata event
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
// Construct the default contact list
|
||||
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
|
||||
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
||||
|
||||
// Publish contact list event
|
||||
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
// Construct the default messaging relay list
|
||||
let relays = default_messaging_relays();
|
||||
|
||||
// Ensure connected to all relays
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
||||
|
||||
// Publish messaging relay list event
|
||||
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
@@ -385,15 +346,20 @@ impl NostrRegistry {
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
// Wait for the task to complete
|
||||
task.await?;
|
||||
|
||||
// Set signer
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(keys, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -472,6 +438,7 @@ impl NostrRegistry {
|
||||
Ok(public_key) => {
|
||||
// Update states
|
||||
this.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
// Add public key to npubs if not already present
|
||||
this.npubs.update(cx, |this, cx| {
|
||||
if !this.contains(&public_key) {
|
||||
@@ -479,22 +446,18 @@ impl NostrRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure relay list for the user
|
||||
this.ensure_relay_list(cx);
|
||||
|
||||
// Emit signer changed event
|
||||
cx.emit(SignerEvent::Set);
|
||||
})?;
|
||||
cx.emit(StateEvent::SignerSet);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -506,16 +469,15 @@ impl NostrRegistry {
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let key_path = keys_dir.join(format!("{}.npub", npub));
|
||||
smol::fs::remove_file(key_path).await?;
|
||||
smol::fs::remove_file(key_path).await.ok();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.npubs().update(cx, |this, cx| {
|
||||
this.retain(|k| k != &public_key);
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -533,16 +495,16 @@ impl NostrRegistry {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(keys, cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -564,192 +526,90 @@ impl NostrRegistry {
|
||||
match task.await {
|
||||
Ok((public_key, uri)) => {
|
||||
let username = public_key.to_bech32().unwrap();
|
||||
let write_credential = this.read_with(cx, |_this, cx| {
|
||||
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
|
||||
})?;
|
||||
let write_credential = this
|
||||
.read_with(cx, |_this, cx| {
|
||||
cx.write_credentials(
|
||||
&username,
|
||||
"nostrconnect",
|
||||
uri.to_string().as_bytes(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match write_credential.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(nip46, cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set the state of the relay list
|
||||
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
|
||||
self.relay_list_state = state;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.verify_relay_list(cx);
|
||||
|
||||
// Set the state to idle before starting the task
|
||||
self.set_relay_state(RelayState::default(), cx);
|
||||
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
let task = self.get_event(public_key, Kind::RelayList, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.relay_list_state = result;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayConnected);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayNotConfigured);
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify relay list for current user
|
||||
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
|
||||
/// Get an event with the given author and kind.
|
||||
pub fn get_event(
|
||||
&self,
|
||||
author: &PublicKey,
|
||||
kind: Kind,
|
||||
cx: &App,
|
||||
) -> Task<Result<Event, Error>> {
|
||||
let client = self.client();
|
||||
let public_key = *author;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![filter.clone()]))
|
||||
.collect();
|
||||
|
||||
// Stream events from the bootstrap relays
|
||||
let filter = Filter::new().kind(kind).author(public_key).limit(1);
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_millis(800))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
log::info!("Received relay list event: {event:?}");
|
||||
return Ok(RelayState::Configured);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
if let Ok(event) = res {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RelayState::NotConfigured)
|
||||
Err(anyhow!("No event found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure write relays for a given public key
|
||||
pub fn ensure_write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let public_key = *public_key;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut relays = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![filter.clone()]))
|
||||
.collect();
|
||||
|
||||
if let Ok(mut stream) = client
|
||||
.stream_events(target)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await
|
||||
{
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
// Extract relay urls
|
||||
relays.extend(nip65::extract_owned_relay_list(event).filter_map(
|
||||
|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == Some(RelayMetadata::Write)
|
||||
{
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Ensure connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
return relays;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a list of write relays for a given public key
|
||||
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let relays = self.gossip.read(cx).write_relays(public_key);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
// Ensure relay connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a list of read relays for a given public key
|
||||
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let relays = self.gossip.read(cx).read_relays(public_key);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
// Ensure relay connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all relays for a given public key without ensuring connections
|
||||
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
||||
self.gossip.read(cx).read_only_relays(public_key)
|
||||
}
|
||||
|
||||
/// Get the public key of a NIP-05 address
|
||||
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||
let client = self.client();
|
||||
@@ -905,8 +765,6 @@ impl NostrRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<SignerEvent> for NostrRegistry {}
|
||||
|
||||
/// Get or create a new app keys
|
||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||
let dir = config_dir().join(".app_keys");
|
||||
@@ -932,52 +790,6 @@ fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
async fn get_events_for_room(client: &Client, nip65: &Event) -> Result<(), Error> {
|
||||
// Subscription options
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)))
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
// Extract write relays from event
|
||||
let write_relays: Vec<&RelayUrl> = nip65::extract_relay_list(nip65)
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Ensure relay connections
|
||||
for url in write_relays.iter() {
|
||||
client.add_relay(*url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let inbox = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(nip65.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct filter for encryption announcement
|
||||
let announcement = Filter::new()
|
||||
.kind(Kind::Custom(10044))
|
||||
.author(nip65.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Vec<Filter>> = write_relays
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![inbox.clone(), announcement.clone()]))
|
||||
.collect();
|
||||
|
||||
// Subscribe to inbox relays and encryption announcements
|
||||
client.subscribe(target).close_on(opts).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
vec![
|
||||
(
|
||||
@@ -1011,43 +823,6 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Signer event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SignerEvent {
|
||||
/// A new signer has been set
|
||||
Set,
|
||||
|
||||
/// An error occurred
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum RelayState {
|
||||
#[default]
|
||||
Idle,
|
||||
Checking,
|
||||
NotConfigured,
|
||||
Configured,
|
||||
}
|
||||
|
||||
impl RelayState {
|
||||
pub fn idle(&self) -> bool {
|
||||
matches!(self, RelayState::Idle)
|
||||
}
|
||||
|
||||
pub fn checking(&self) -> bool {
|
||||
matches!(self, RelayState::Checking)
|
||||
}
|
||||
|
||||
pub fn not_configured(&self) -> bool {
|
||||
matches!(self, RelayState::NotConfigured)
|
||||
}
|
||||
|
||||
pub fn configured(&self) -> bool {
|
||||
matches!(self, RelayState::Configured)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoopAuthUrlHandler;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{hsla, Hsla, Rgba};
|
||||
use gpui::{Hsla, Rgba, hsla};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -30,6 +30,8 @@ pub struct ThemeColors {
|
||||
pub text_muted: Hsla,
|
||||
pub text_placeholder: Hsla,
|
||||
pub text_accent: Hsla,
|
||||
pub text_danger: Hsla,
|
||||
pub text_warning: Hsla,
|
||||
|
||||
// Icon colors
|
||||
pub icon: Hsla,
|
||||
@@ -77,11 +79,11 @@ pub struct ThemeColors {
|
||||
pub ghost_element_disabled: Hsla,
|
||||
|
||||
// Tab colors
|
||||
pub tab_inactive_background: Hsla,
|
||||
pub tab_inactive_foreground: Hsla,
|
||||
pub tab_background: Hsla,
|
||||
pub tab_foreground: Hsla,
|
||||
pub tab_hover_background: Hsla,
|
||||
pub tab_active_background: Hsla,
|
||||
pub tab_active_foreground: Hsla,
|
||||
pub tab_hover_foreground: Hsla,
|
||||
|
||||
// Scrollbar colors
|
||||
pub scrollbar_thumb_background: Hsla,
|
||||
@@ -110,8 +112,8 @@ impl ThemeColors {
|
||||
elevated_surface_background: neutral().light().step_3(),
|
||||
panel_background: neutral().light().step_1(),
|
||||
overlay: neutral().light_alpha().step_3(),
|
||||
title_bar: neutral().light().step_2(),
|
||||
title_bar_inactive: neutral().light().step_3(),
|
||||
title_bar: neutral().light().step_3(),
|
||||
title_bar_inactive: neutral().light().step_1(),
|
||||
window_border: hsl(240.0, 5.9, 78.0),
|
||||
|
||||
border: neutral().light().step_6(),
|
||||
@@ -125,7 +127,9 @@ impl ThemeColors {
|
||||
text: neutral().light().step_12(),
|
||||
text_muted: neutral().light().step_11(),
|
||||
text_placeholder: neutral().light().step_10(),
|
||||
text_accent: brand().light().step_11(),
|
||||
text_accent: brand().light().step_9(),
|
||||
text_danger: danger().light().step_9(),
|
||||
text_warning: warning().light().step_9(),
|
||||
|
||||
icon: neutral().light().step_11(),
|
||||
icon_muted: neutral().light().step_10(),
|
||||
@@ -166,17 +170,17 @@ impl ThemeColors {
|
||||
ghost_element_selected: neutral().light().step_5(),
|
||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||
|
||||
tab_inactive_background: neutral().light().step_2(),
|
||||
tab_inactive_foreground: neutral().light().step_11(),
|
||||
tab_background: neutral().light().step_3(),
|
||||
tab_foreground: neutral().light().step_11(),
|
||||
tab_hover_background: neutral().light_alpha().step_4(),
|
||||
tab_active_background: neutral().light().step_1(),
|
||||
tab_active_foreground: neutral().light().step_12(),
|
||||
tab_hover_foreground: brand().light().step_9(),
|
||||
|
||||
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
||||
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
||||
scrollbar_thumb_border: gpui::transparent_black(),
|
||||
scrollbar_track_background: gpui::transparent_black(),
|
||||
scrollbar_track_border: neutral().light().step_5(),
|
||||
scrollbar_track_border: gpui::transparent_black(),
|
||||
|
||||
drop_target_background: brand().light_alpha().step_2(),
|
||||
cursor: hsl(200., 100., 50.),
|
||||
@@ -192,9 +196,9 @@ impl ThemeColors {
|
||||
background: neutral().dark().step_1(),
|
||||
surface_background: neutral().dark().step_2(),
|
||||
elevated_surface_background: neutral().dark().step_3(),
|
||||
panel_background: gpui::black(),
|
||||
panel_background: neutral().dark().step_1(),
|
||||
overlay: neutral().dark_alpha().step_3(),
|
||||
title_bar: gpui::transparent_black(),
|
||||
title_bar: neutral().dark().step_3(),
|
||||
title_bar_inactive: neutral().dark().step_1(),
|
||||
window_border: hsl(240.0, 3.7, 28.0),
|
||||
|
||||
@@ -209,7 +213,9 @@ impl ThemeColors {
|
||||
text: neutral().dark().step_12(),
|
||||
text_muted: neutral().dark().step_11(),
|
||||
text_placeholder: neutral().dark().step_10(),
|
||||
text_accent: brand().dark().step_11(),
|
||||
text_accent: brand().dark().step_9(),
|
||||
text_danger: danger().dark().step_9(),
|
||||
text_warning: warning().dark().step_9(),
|
||||
|
||||
icon: neutral().dark().step_11(),
|
||||
icon_muted: neutral().dark().step_10(),
|
||||
@@ -250,17 +256,17 @@ impl ThemeColors {
|
||||
ghost_element_selected: neutral().dark().step_5(),
|
||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||
|
||||
tab_inactive_background: neutral().dark().step_2(),
|
||||
tab_inactive_foreground: neutral().dark().step_11(),
|
||||
tab_active_background: neutral().dark().step_3(),
|
||||
tab_background: neutral().dark().step_3(),
|
||||
tab_foreground: neutral().dark().step_11(),
|
||||
tab_hover_background: neutral().dark_alpha().step_4(),
|
||||
tab_active_background: neutral().dark().step_1(),
|
||||
tab_active_foreground: neutral().dark().step_12(),
|
||||
tab_hover_foreground: brand().dark().step_9(),
|
||||
|
||||
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
||||
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
||||
scrollbar_thumb_border: gpui::transparent_black(),
|
||||
scrollbar_track_background: gpui::transparent_black(),
|
||||
scrollbar_track_border: neutral().dark().step_5(),
|
||||
scrollbar_track_border: gpui::transparent_black(),
|
||||
|
||||
drop_target_background: brand().dark_alpha().step_2(),
|
||||
cursor: hsl(200., 100., 50.),
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
||||
pub fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
||||
match axis {
|
||||
Axis::Vertical => match self {
|
||||
Self::TopLeft => Self::BottomLeft,
|
||||
@@ -4,6 +4,8 @@ use std::rc::Rc;
|
||||
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||
|
||||
mod colors;
|
||||
mod geometry;
|
||||
mod notification;
|
||||
mod platform_kind;
|
||||
mod registry;
|
||||
mod scale;
|
||||
@@ -11,6 +13,8 @@ mod scrollbar_mode;
|
||||
mod theme;
|
||||
|
||||
pub use colors::*;
|
||||
pub use geometry::*;
|
||||
pub use notification::*;
|
||||
pub use platform_kind::PlatformKind;
|
||||
pub use registry::*;
|
||||
pub use scale::*;
|
||||
@@ -82,6 +86,9 @@ pub struct Theme {
|
||||
/// Show the scrollbar mode, default: scrolling
|
||||
pub scrollbar_mode: ScrollbarMode,
|
||||
|
||||
/// Notification settings
|
||||
pub notification: NotificationSettings,
|
||||
|
||||
/// Platform kind
|
||||
pub platform: PlatformKind,
|
||||
}
|
||||
@@ -200,10 +207,11 @@ impl From<ThemeFamily> for Theme {
|
||||
Theme {
|
||||
font_size: px(15.),
|
||||
font_family: font_family.into(),
|
||||
radius: px(5.),
|
||||
radius: px(6.),
|
||||
radius_lg: px(10.),
|
||||
shadow: true,
|
||||
scrollbar_mode: ScrollbarMode::default(),
|
||||
notification: NotificationSettings::default(),
|
||||
mode,
|
||||
colors: *colors,
|
||||
theme: Rc::new(family),
|
||||
|
||||
31
crates/theme/src/notification.rs
Normal file
31
crates/theme/src/notification.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use gpui::{Pixels, px};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Anchor, Edges, TITLEBAR_HEIGHT};
|
||||
|
||||
/// The settings for notifications.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NotificationSettings {
|
||||
/// The placement of the notification, default: [`Anchor::TopRight`]
|
||||
pub placement: Anchor,
|
||||
/// The margins of the notification with respect to the window edges.
|
||||
pub margins: Edges<Pixels>,
|
||||
/// The maximum number of notifications to show at once, default: 10
|
||||
pub max_items: usize,
|
||||
}
|
||||
|
||||
impl Default for NotificationSettings {
|
||||
fn default() -> Self {
|
||||
let offset = px(16.);
|
||||
Self {
|
||||
placement: Anchor::TopRight,
|
||||
margins: Edges {
|
||||
top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar
|
||||
right: offset,
|
||||
bottom: offset,
|
||||
left: offset,
|
||||
},
|
||||
max_items: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
//! This is a fork of gpui's anchored element that adds support for offsetting
|
||||
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
|
||||
use gpui::{
|
||||
point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
|
||||
AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
|
||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
||||
Window,
|
||||
Window, point, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Anchor;
|
||||
use theme::Anchor;
|
||||
|
||||
/// The state that the anchored element element uses to track its children.
|
||||
pub struct AnchoredState {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement,
|
||||
Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
|
||||
Window,
|
||||
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||
px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
|
||||
/// Returns the size of the avatar based on the given [`Size`].
|
||||
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
@@ -37,6 +37,7 @@ pub struct Avatar {
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
@@ -48,6 +49,7 @@ impl Avatar {
|
||||
style: StyleRefinement::default(),
|
||||
size: Size::Medium,
|
||||
border_color: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +91,17 @@ impl Styled for Avatar {
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Avatar {
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Avatar {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.base.interactivity()
|
||||
|
||||
@@ -3,20 +3,26 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
|
||||
Window,
|
||||
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{DockArea, DockItem};
|
||||
use crate::dock_area::panel::PanelView;
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
|
||||
use crate::StyledExt;
|
||||
use crate::dock::panel::PanelView;
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
|
||||
|
||||
#[derive(Clone, Render)]
|
||||
#[derive(Clone)]
|
||||
struct ResizePanel;
|
||||
|
||||
impl Render for ResizePanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DockPlacement {
|
||||
Center,
|
||||
@@ -321,6 +327,8 @@ impl Render for Dock {
|
||||
return div();
|
||||
}
|
||||
|
||||
let cache_style = StyleRefinement::default().absolute().size_full();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
@@ -336,7 +344,7 @@ impl Render for Dock {
|
||||
.map(|this| match &self.panel {
|
||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||
DockItem::Panel { view, .. } => this.child(view.clone().view()),
|
||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
||||
})
|
||||
.child(self.render_resize_handle(window, cx))
|
||||
.child(DockElement {
|
||||
@@ -2,22 +2,24 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
|
||||
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
||||
};
|
||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||
|
||||
use crate::dock_area::dock::{Dock, DockPlacement};
|
||||
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
||||
use crate::dock_area::stack_panel::StackPanel;
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::ElementExt;
|
||||
|
||||
pub mod dock;
|
||||
pub mod panel;
|
||||
pub mod stack_panel;
|
||||
pub mod tab_panel;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod dock;
|
||||
mod panel;
|
||||
mod stack_panel;
|
||||
mod tab_panel;
|
||||
|
||||
pub use dock::*;
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use tab_panel::*;
|
||||
|
||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
|
||||
@@ -21,12 +21,6 @@ pub enum PanelStyle {
|
||||
TabBar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TitleStyle {
|
||||
pub background: Hsla,
|
||||
pub foreground: Hsla,
|
||||
}
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||
///
|
||||
@@ -7,16 +7,16 @@ use gpui::{
|
||||
Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
|
||||
|
||||
use super::{DockArea, PanelEvent};
|
||||
use crate::dock_area::panel::{Panel, PanelView};
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::dock::panel::{Panel, PanelView};
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::h_flex;
|
||||
use crate::resizable::{
|
||||
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||
PANEL_MIN_SIZE,
|
||||
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||
resizable_panel,
|
||||
};
|
||||
use crate::{h_flex, AxisExt as _, Placement};
|
||||
|
||||
pub struct StackPanel {
|
||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
||||
@@ -2,22 +2,22 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||
WeakEntity, Window, div, px, rems,
|
||||
};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
use crate::dock_area::panel::{Panel, PanelView};
|
||||
use crate::dock_area::stack_panel::StackPanel;
|
||||
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||
use crate::dock::dock::DockPlacement;
|
||||
use crate::dock::panel::{Panel, PanelView};
|
||||
use crate::dock::stack_panel::StackPanel;
|
||||
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||
use crate::menu::{DropdownMenu, PopupMenu};
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::tab::Tab;
|
||||
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TabState {
|
||||
@@ -42,22 +42,20 @@ impl DragPanel {
|
||||
|
||||
impl Render for DragPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
h_flex()
|
||||
.id("drag-panel")
|
||||
.cursor_grab()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.p_2()
|
||||
.min_w_24()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.when(cx.theme().shadow, |this| this.shadow_lg())
|
||||
.text_color(cx.theme().text)
|
||||
.text_ellipsis()
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
.bg(cx.theme().background)
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(self.panel.title(cx))
|
||||
}
|
||||
}
|
||||
@@ -425,14 +423,13 @@ impl TabPanel {
|
||||
let view = cx.entity().clone();
|
||||
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
||||
let toolbar = self.toolbar_buttons(window, cx);
|
||||
let has_toolbar = !toolbar.is_empty();
|
||||
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.occlude()
|
||||
.rounded_full()
|
||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
|
||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost()))
|
||||
.when(self.zoomed, |this| {
|
||||
this.child(
|
||||
Button::new("zoom")
|
||||
@@ -445,15 +442,11 @@ impl TabPanel {
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(has_toolbar, |this| {
|
||||
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
})
|
||||
.child(
|
||||
Button::new("menu")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded()
|
||||
.dropdown_menu({
|
||||
let zoomable = state.zoomable;
|
||||
let closable = state.closable;
|
||||
@@ -578,6 +571,7 @@ impl TabPanel {
|
||||
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
||||
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||
let tabs_count = self.panels.len();
|
||||
let is_bottom_dock = bottom_dock_button.is_some();
|
||||
|
||||
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
||||
let panel = self.panels.first().unwrap();
|
||||
@@ -646,7 +640,7 @@ impl TabPanel {
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
TabBar::new()
|
||||
TabBar::new("tab-bar")
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.when(has_extend_dock_button, |this| {
|
||||
@@ -659,8 +653,9 @@ impl TabPanel {
|
||||
.border_b_1()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.px_2()
|
||||
.bg(cx.theme().tab_background)
|
||||
.pl_0p5()
|
||||
.pr_1()
|
||||
.children(left_dock_button)
|
||||
.children(bottom_dock_button),
|
||||
)
|
||||
@@ -682,16 +677,43 @@ impl TabPanel {
|
||||
Some(
|
||||
Tab::new()
|
||||
.ix(ix)
|
||||
.label(panel.title(cx))
|
||||
.py_2()
|
||||
.tab_bar_prefix(has_extend_dock_button)
|
||||
.child(panel.title(cx))
|
||||
.selected(active)
|
||||
.disabled(disabled)
|
||||
.suffix(
|
||||
Button::new("close-{ix}")
|
||||
.icon(IconName::Close)
|
||||
.tooltip("Close panel")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener({
|
||||
let panel = panel.clone();
|
||||
move |view, _ev, window, cx| {
|
||||
view.remove_panel(&panel, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let is_collapsed = self.collapsed;
|
||||
let dock_area = self.dock_area.clone();
|
||||
move |view, _, window, cx| {
|
||||
view.set_active_ix(ix, window, cx);
|
||||
|
||||
// Open dock if clicked on the collapsed bottom dock
|
||||
if is_bottom_dock && is_collapsed {
|
||||
_ = dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
.when(!disabled, |this| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
cx.listener({
|
||||
let panel = panel.clone();
|
||||
move |view, _, window, cx| {
|
||||
move |view, _ev, window, cx| {
|
||||
view.remove_panel(&panel, window, cx);
|
||||
}
|
||||
}),
|
||||
@@ -757,14 +779,15 @@ impl TabPanel {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.border_l_1()
|
||||
.border_b_1()
|
||||
.px_0p5()
|
||||
.gap_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tab_background)
|
||||
.child(self.render_toolbar(state, window, cx))
|
||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||
)
|
||||
@@ -1099,6 +1122,7 @@ impl Focusable for TabPanel {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||
|
||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||
|
||||
impl Render for TabPanel {
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub enum IconName {
|
||||
Ellipsis,
|
||||
Emoji,
|
||||
Eye,
|
||||
Input,
|
||||
Info,
|
||||
Invite,
|
||||
Inbox,
|
||||
@@ -110,6 +111,7 @@ impl IconNamed for IconName {
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
|
||||
@@ -2,11 +2,10 @@ pub use anchored::*;
|
||||
pub use element_ext::ElementExt;
|
||||
pub use event::InteractiveElementExt;
|
||||
pub use focusable::FocusableCycle;
|
||||
pub use geometry::*;
|
||||
pub use icon::*;
|
||||
pub use index_path::IndexPath;
|
||||
pub use kbd::*;
|
||||
pub use root::{window_paddings, Root};
|
||||
pub use root::{Root, window_paddings};
|
||||
pub use styled::*;
|
||||
pub use window_ext::*;
|
||||
|
||||
@@ -18,7 +17,7 @@ pub mod avatar;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod divider;
|
||||
pub mod dock_area;
|
||||
pub mod dock;
|
||||
pub mod group_box;
|
||||
pub mod history;
|
||||
pub mod indicator;
|
||||
@@ -39,7 +38,6 @@ mod anchored;
|
||||
mod element_ext;
|
||||
mod event;
|
||||
mod focusable;
|
||||
mod geometry;
|
||||
mod icon;
|
||||
mod index_path;
|
||||
mod kbd;
|
||||
|
||||
@@ -5,10 +5,11 @@ use gpui::{
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::Selectable;
|
||||
use crate::avatar::Avatar;
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::popover::Popover;
|
||||
use crate::Selectable;
|
||||
|
||||
/// A dropdown menu trait for buttons and other interactive elements
|
||||
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
||||
@@ -35,6 +36,8 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement +
|
||||
|
||||
impl DropdownMenu for Button {}
|
||||
|
||||
impl DropdownMenu for Avatar {}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||
id: ElementId,
|
||||
|
||||
@@ -2,19 +2,19 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent,
|
||||
Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
|
||||
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, WeakEntity, Window,
|
||||
Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent,
|
||||
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
||||
KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
|
||||
div, px, rems,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||
use crate::kbd::Kbd;
|
||||
use crate::menu::menu_item::MenuItemElement;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{h_flex, v_flex, ElementExt, Icon, IconName, Side, Sizable as _, Size, StyledExt};
|
||||
use crate::{ElementExt, Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "PopupMenu";
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
|
||||
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
||||
Window,
|
||||
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -14,7 +13,7 @@ use crate::actions::{Cancel, Confirm};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
|
||||
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Modal";
|
||||
|
||||
@@ -500,6 +499,7 @@ impl RenderOnce for Modal {
|
||||
.child(self.content),
|
||||
),
|
||||
)
|
||||
.when_none(&self.footer, |this| this.child(div().pt(padding_left)))
|
||||
.when_some(self.footer, |this, footer| {
|
||||
this.child(
|
||||
h_flex()
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Window,
|
||||
Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
|
||||
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||
Window, div, px, relative,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Anchor};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||
use crate::{Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum NotificationType {
|
||||
pub enum NotificationKind {
|
||||
#[default]
|
||||
Info,
|
||||
Success,
|
||||
@@ -27,13 +25,15 @@ pub enum NotificationType {
|
||||
Error,
|
||||
}
|
||||
|
||||
impl NotificationType {
|
||||
impl NotificationKind {
|
||||
fn icon(&self, cx: &App) -> Icon {
|
||||
match self {
|
||||
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground),
|
||||
Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground),
|
||||
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
|
||||
Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground),
|
||||
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
|
||||
Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
|
||||
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().text_warning),
|
||||
Self::Error => {
|
||||
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ impl From<(TypeId, ElementId)> for NotificationId {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
@@ -64,16 +65,13 @@ pub struct Notification {
|
||||
/// None means the notification will be added to the end of the list.
|
||||
id: NotificationId,
|
||||
style: StyleRefinement,
|
||||
type_: Option<NotificationType>,
|
||||
kind: Option<NotificationKind>,
|
||||
title: Option<SharedString>,
|
||||
message: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
autohide: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
||||
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
closing: bool,
|
||||
}
|
||||
@@ -84,12 +82,6 @@ impl From<String> for Notification {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'static, str>> for Notification {
|
||||
fn from(s: Cow<'static, str>) -> Self {
|
||||
Self::new().message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Notification {
|
||||
fn from(s: SharedString) -> Self {
|
||||
Self::new().message(s)
|
||||
@@ -102,24 +94,24 @@ impl From<&'static str> for Notification {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, &'static str)> for Notification {
|
||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
||||
Self::new().message(content).with_type(type_)
|
||||
impl From<(NotificationKind, &'static str)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, &'static str)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, SharedString)> for Notification {
|
||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
||||
Self::new().message(content).with_type(type_)
|
||||
impl From<(NotificationKind, SharedString)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, SharedString)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification with the given content.
|
||||
/// Create a new notification.
|
||||
///
|
||||
/// default width is 320px.
|
||||
/// The default id is a random UUID.
|
||||
pub fn new() -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
@@ -129,7 +121,7 @@ impl Notification {
|
||||
style: StyleRefinement::default(),
|
||||
title: None,
|
||||
message: None,
|
||||
type_: None,
|
||||
kind: None,
|
||||
icon: None,
|
||||
autohide: true,
|
||||
action_builder: None,
|
||||
@@ -139,33 +131,38 @@ impl Notification {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the message of the notification, default is None.
|
||||
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an info notification with the given message.
|
||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Info)
|
||||
.with_kind(NotificationKind::Info)
|
||||
}
|
||||
|
||||
/// Create a success notification with the given message.
|
||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Success)
|
||||
.with_kind(NotificationKind::Success)
|
||||
}
|
||||
|
||||
/// Create a warning notification with the given message.
|
||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Warning)
|
||||
.with_kind(NotificationKind::Warning)
|
||||
}
|
||||
|
||||
/// Create an error notification with the given message.
|
||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Error)
|
||||
.with_kind(NotificationKind::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
@@ -180,8 +177,8 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||
pub fn custom_id(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
||||
pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -202,8 +199,8 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
||||
self.type_ = Some(type_);
|
||||
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
|
||||
self.kind = Some(kind);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -223,22 +220,31 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the action button of the notification.
|
||||
///
|
||||
/// When an action is set, the notification will not autohide.
|
||||
pub fn action<F>(mut self, action: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
{
|
||||
self.action_builder = Some(Rc::new(action));
|
||||
self.autohide = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.closing {
|
||||
return;
|
||||
}
|
||||
self.closing = true;
|
||||
cx.notify();
|
||||
|
||||
// Dismiss the notification after 0.15s to show the animation.
|
||||
cx.spawn(async move |view, cx| {
|
||||
Timer::after(Duration::from_secs_f32(0.15)).await;
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs_f32(0.15))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
@@ -248,13 +254,13 @@ impl Notification {
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach()
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Set the content of the notification.
|
||||
pub fn content(
|
||||
mut self,
|
||||
content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
) -> Self {
|
||||
self.content_builder = Some(Rc::new(content));
|
||||
self
|
||||
@@ -276,57 +282,76 @@ impl Styled for Notification {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let closing = self.closing;
|
||||
let icon = match self.type_ {
|
||||
let placement = cx.theme().notification.placement;
|
||||
|
||||
let content = self
|
||||
.content_builder
|
||||
.clone()
|
||||
.map(|builder| builder(self, window, cx));
|
||||
|
||||
let action = self
|
||||
.action_builder
|
||||
.clone()
|
||||
.map(|builder| builder(self, window, cx).small().mr_3p5());
|
||||
|
||||
let icon = match self.kind {
|
||||
None => self.icon.clone(),
|
||||
Some(type_) => Some(type_.icon(cx)),
|
||||
Some(kind) => Some(kind.icon(cx)),
|
||||
};
|
||||
|
||||
let background = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_background,
|
||||
_ => cx.theme().surface_background,
|
||||
};
|
||||
|
||||
let text_color = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_foreground,
|
||||
_ => cx.theme().text,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("notification")
|
||||
.refine_style(&self.style)
|
||||
.group("")
|
||||
.occlude()
|
||||
.relative()
|
||||
.w_96()
|
||||
.w_112()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.bg(background)
|
||||
.text_color(text_color)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.p_2()
|
||||
.gap_3()
|
||||
.gap_2()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.refine_style(&self.style)
|
||||
.when_some(icon, |this, icon| {
|
||||
this.child(div().flex_shrink_0().pt_1().child(icon))
|
||||
this.child(div().flex_shrink_0().child(icon))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(div().text_sm().font_semibold().child(title))
|
||||
})
|
||||
.when_some(self.message.clone(), |this, message| {
|
||||
this.child(div().text_sm().child(message))
|
||||
this.child(div().text_sm().line_height(relative(1.25)).child(message))
|
||||
})
|
||||
.when_some(self.content_builder.clone(), |this, child_builder| {
|
||||
this.child(child_builder(window, cx))
|
||||
})
|
||||
.when_some(self.action_builder.clone(), |this, action_builder| {
|
||||
this.child(action_builder(window, cx).small().w_full().my_2())
|
||||
.when_some(content, |this, content| this.child(content))
|
||||
.when_some(action, |this, action| {
|
||||
this.child(h_flex().flex_1().gap_1().justify_end().child(action))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_2p5()
|
||||
.right_2p5()
|
||||
.top(px(6.5))
|
||||
.right(px(6.5))
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
@@ -334,7 +359,7 @@ impl Render for Notification {
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.dismiss(window, cx);
|
||||
})),
|
||||
),
|
||||
@@ -345,21 +370,47 @@ impl Render for Notification {
|
||||
on_click(event, window, cx);
|
||||
}))
|
||||
})
|
||||
.on_aux_click(cx.listener(move |view, event: &ClickEvent, window, cx| {
|
||||
if event.is_middle_click() {
|
||||
view.dismiss(window, cx);
|
||||
}
|
||||
}))
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
let opacity = 1. - delta;
|
||||
this.left(px(0.) + x_offset)
|
||||
let that = this
|
||||
.shadow_none()
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none())
|
||||
.when(opacity < 0.85, |this| this.shadow_none());
|
||||
match placement {
|
||||
Anchor::TopRight | Anchor::BottomRight => {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopLeft | Anchor::BottomLeft => {
|
||||
let x_offset = px(0.) - delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopCenter => {
|
||||
let y_offset = px(0.) - delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
Anchor::BottomCenter => {
|
||||
let y_offset = px(0.) + delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let y_offset = px(-45.) + delta * px(45.);
|
||||
let opacity = delta;
|
||||
let y_offset = match placement {
|
||||
placement if placement.is_top() => px(-45.) + delta * px(45.),
|
||||
placement if placement.is_bottom() => px(45.) - delta * px(45.),
|
||||
_ => px(0.),
|
||||
};
|
||||
this.top(px(0.) + y_offset)
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none())
|
||||
@@ -373,7 +424,11 @@ impl Render for Notification {
|
||||
pub struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||
|
||||
/// Whether the notification list is expanded.
|
||||
expanded: bool,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
@@ -386,10 +441,12 @@ impl NotificationList {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<Notification>,
|
||||
{
|
||||
pub fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let notification = notification.into();
|
||||
let id = notification.id.clone();
|
||||
let autohide = notification.autohide;
|
||||
@@ -411,36 +468,35 @@ impl NotificationList {
|
||||
|
||||
if autohide {
|
||||
// Sleep for 5 seconds to autohide the notification
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
Timer::after(Duration::from_secs(5)).await;
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||
|
||||
if let Err(error) =
|
||||
if let Err(err) =
|
||||
notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
|
||||
{
|
||||
log::error!("Failed to auto hide notification: {error}");
|
||||
log::error!("failed to auto hide notification: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<ElementId>,
|
||||
{
|
||||
let id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
||||
|
||||
pub(crate) fn close(
|
||||
&mut self,
|
||||
id: impl Into<NotificationId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id: NotificationId = id.into();
|
||||
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
||||
n.update(cx, |note, cx| {
|
||||
note.dismiss(window, cx);
|
||||
});
|
||||
n.update(cx, |note, cx| note.dismiss(window, cx))
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.notifications.clear();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -451,25 +507,46 @@ impl NotificationList {
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let size = window.viewport_size();
|
||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||
|
||||
div()
|
||||
.id("notification-wrapper")
|
||||
.absolute()
|
||||
.top_4()
|
||||
.right_4()
|
||||
.child(
|
||||
let placement = cx.theme().notification.placement;
|
||||
let margins = &cx.theme().notification.margins;
|
||||
|
||||
v_flex()
|
||||
.id("notification-list")
|
||||
.h(size.height - px(8.))
|
||||
.max_h(size.height)
|
||||
.pt(margins.top)
|
||||
.pb(margins.bottom)
|
||||
.gap_3()
|
||||
.children(items)
|
||||
.when(
|
||||
matches!(placement, Anchor::TopRight),
|
||||
|this| this.pr(margins.right), // ignore left
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::TopLeft),
|
||||
|this| this.pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomLeft),
|
||||
|this| this.flex_col_reverse().pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomRight),
|
||||
|this| this.flex_col_reverse().pr(margins.right), // ignore left
|
||||
)
|
||||
.when(matches!(placement, Anchor::BottomCenter), |this| {
|
||||
this.flex_col_reverse()
|
||||
})
|
||||
.on_hover(cx.listener(|view, hovered, _, cx| {
|
||||
view.expanded = *hovered;
|
||||
cx.notify()
|
||||
})),
|
||||
)
|
||||
}))
|
||||
.children(items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
|
||||
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
|
||||
Styled, Subscription, Window,
|
||||
AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
|
||||
FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
||||
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
|
||||
Subscription, Window, deferred, div, px,
|
||||
};
|
||||
use theme::Anchor;
|
||||
|
||||
use crate::actions::Cancel;
|
||||
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
|
||||
use crate::{ElementExt, Selectable, StyledExt as _, anchored, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Popover";
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty,
|
||||
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
|
||||
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
||||
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div,
|
||||
};
|
||||
use theme::AxisExt;
|
||||
|
||||
use super::{resizable_panel, resize_handle, ResizableState};
|
||||
use super::{ResizableState, resizable_panel, resize_handle};
|
||||
use crate::resizable::PANEL_MIN_SIZE;
|
||||
use crate::{h_flex, v_flex, AxisExt, ElementExt};
|
||||
use crate::{ElementExt, h_flex, v_flex};
|
||||
|
||||
pub enum ResizablePanelEvent {
|
||||
Resized,
|
||||
|
||||
@@ -3,14 +3,13 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId,
|
||||
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels,
|
||||
Point, Render, StatefulInteractiveElement, Styled as _, Window,
|
||||
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
||||
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
StatefulInteractiveElement, Styled as _, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, AxisExt};
|
||||
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
use crate::AxisExt;
|
||||
use crate::dock::DockPlacement;
|
||||
|
||||
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
||||
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::any::TypeId;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, Entity,
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity,
|
||||
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, Tiling,
|
||||
WeakFocusHandle, Window, canvas, div, point, px, size,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
|
||||
Window, canvas, div, point, px, size,
|
||||
};
|
||||
use theme::{
|
||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||
@@ -213,13 +214,30 @@ impl Root {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its ID.
|
||||
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
self.notification
|
||||
.update(cx, |view, cx| view.close(id.into(), window, cx));
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification<T: Sized + 'static>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = TypeId::of::<T>();
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = (TypeId::of::<T>(), key.into());
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use gpui::{
|
||||
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
||||
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
||||
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
|
||||
};
|
||||
|
||||
use crate::AxisExt;
|
||||
use theme::AxisExt;
|
||||
|
||||
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
||||
///
|
||||
|
||||
@@ -11,9 +11,7 @@ use gpui::{
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
||||
point, px, relative, size,
|
||||
};
|
||||
use theme::{ActiveTheme, ScrollbarMode};
|
||||
|
||||
use crate::AxisExt;
|
||||
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
||||
|
||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
||||
@@ -54,7 +52,7 @@ impl ScrollbarHandle for ScrollHandle {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.max_offset() + self.bounds().size
|
||||
Size::from(self.max_offset()) + self.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +67,7 @@ impl ScrollbarHandle for UniformListScrollHandle {
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let base_handle = &self.0.borrow().base_handle;
|
||||
base_handle.max_offset() + base_handle.bounds().size
|
||||
Size::from(base_handle.max_offset()) + base_handle.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +81,7 @@ impl ScrollbarHandle for ListState {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.viewport_bounds().size + self.max_offset_for_scrollbar()
|
||||
Size::from(self.max_offset_for_scrollbar()) + self.viewport_bounds().size
|
||||
}
|
||||
|
||||
fn start_drag(&self) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
|
||||
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
|
||||
/// Apply paddings to the element.
|
||||
fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let paddings = paddings.into();
|
||||
self.pt(paddings.top.into())
|
||||
.pb(paddings.bottom.into())
|
||||
.pl(paddings.left.into())
|
||||
.pr(paddings.right.into())
|
||||
}
|
||||
|
||||
/// Apply margins to the element.
|
||||
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let margins = margins.into();
|
||||
self.mt(margins.top.into())
|
||||
.mb(margins.bottom.into())
|
||||
.ml(margins.left.into())
|
||||
.mr(margins.right.into())
|
||||
}
|
||||
|
||||
font_weight!(font_thin, THIN);
|
||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||
font_weight!(font_light, LIGHT);
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId,
|
||||
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString,
|
||||
Styled as _, Window,
|
||||
Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
||||
Window, div, px, white,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::{Disableable, Side, Sizable, Size};
|
||||
use crate::{Disableable, Sizable, Size};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
|
||||
@@ -1,74 +1,557 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, px, relative,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
pub mod tab_bar;
|
||||
|
||||
/// Tab variants.
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TabVariant {
|
||||
#[default]
|
||||
Tab,
|
||||
Outline,
|
||||
Pill,
|
||||
Segmented,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl TabVariant {
|
||||
fn height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => px(26.),
|
||||
_ => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => px(30.),
|
||||
_ => px(24.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => px(44.),
|
||||
_ => px(36.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => px(36.),
|
||||
_ => px(32.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
|
||||
TabVariant::Segmented => px(16.),
|
||||
TabVariant::Underline => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
|
||||
TabVariant::Segmented => px(18.),
|
||||
TabVariant::Underline => px(22.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
|
||||
TabVariant::Segmented => px(28.),
|
||||
TabVariant::Underline => px(32.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Tab => px(30.),
|
||||
TabVariant::Outline | TabVariant::Pill => px(26.),
|
||||
TabVariant::Segmented => px(24.),
|
||||
TabVariant::Underline => px(26.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
|
||||
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
|
||||
let mut padding_x = match size {
|
||||
Size::XSmall => px(8.),
|
||||
Size::Small => px(10.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
|
||||
if matches!(self, TabVariant::Underline) {
|
||||
padding_x = px(0.);
|
||||
}
|
||||
|
||||
Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(1.),
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(2.),
|
||||
bottom: px(3.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(5.),
|
||||
bottom: px(6.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(3.),
|
||||
bottom: px(4.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().text,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_hover,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn selected(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().tab_active_background,
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_accent,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().element_foreground,
|
||||
bg: cx.theme().element_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: cx.theme().background,
|
||||
shadow: true,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: if selected {
|
||||
cx.theme().element_active
|
||||
} else {
|
||||
cx.theme().border
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: if selected {
|
||||
cx.theme().element_foreground.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
},
|
||||
bg: if selected {
|
||||
cx.theme().element_background.opacity(0.5)
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: cx.theme().tab_background,
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
if *self != TabVariant::Segmented {
|
||||
return px(0.);
|
||||
}
|
||||
|
||||
match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
}
|
||||
}
|
||||
|
||||
fn radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Outline | TabVariant::Pill => px(99.),
|
||||
TabVariant::Segmented => match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Segmented => match size {
|
||||
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
|
||||
_ => self.tab_bar_radius(size, cx) - px(2.),
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct TabStyle {
|
||||
borders: Edges<Pixels>,
|
||||
border_color: Hsla,
|
||||
bg: Hsla,
|
||||
fg: Hsla,
|
||||
shadow: bool,
|
||||
inner_bg: Hsla,
|
||||
}
|
||||
|
||||
impl Default for TabStyle {
|
||||
fn default() -> Self {
|
||||
TabStyle {
|
||||
borders: Edges::all(px(0.)),
|
||||
border_color: gpui::transparent_white(),
|
||||
bg: gpui::transparent_white(),
|
||||
fg: gpui::transparent_white(),
|
||||
shadow: false,
|
||||
inner_bg: gpui::transparent_white(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A Tab element for the [`super::TabBar`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tab {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
label: Option<AnyElement>,
|
||||
pub(super) label: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
prefix: Option<AnyElement>,
|
||||
pub(super) tab_bar_prefix: Option<bool>,
|
||||
suffix: Option<AnyElement>,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
children: Vec<AnyElement>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
size: Size::default(),
|
||||
impl From<&'static str> for Tab {
|
||||
fn from(label: &'static str) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set label for the tab.
|
||||
pub fn label(mut self, label: impl Into<AnyElement>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
impl From<String> for Tab {
|
||||
fn from(label: String) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
self
|
||||
impl From<SharedString> for Tab {
|
||||
fn from(label: SharedString) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
|
||||
self.suffix = Some(suffix.into());
|
||||
self
|
||||
impl From<Icon> for Tab {
|
||||
fn from(icon: Icon) -> Self {
|
||||
Self::default().icon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index to the tab.
|
||||
pub fn ix(mut self, ix: usize) -> Self {
|
||||
self.ix = ix;
|
||||
self
|
||||
impl From<IconName> for Tab {
|
||||
fn from(icon_name: IconName) -> Self {
|
||||
Self::default().icon(Icon::new(icon_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
icon: None,
|
||||
tab_bar_prefix: None,
|
||||
children: Vec::new(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
/// Create a new tab with a label.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set label for the tab.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set icon for the tab.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set Tab Variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Pill variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use outline variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Segmented variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Underline variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab, default false.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the tab.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index to the tab.
|
||||
pub(crate) fn ix(mut self, ix: usize) -> Self {
|
||||
self.ix = ix;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set if the tab bar has a prefix.
|
||||
pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
|
||||
self.tab_bar_prefix = Some(tab_bar_prefix);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Tab {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,62 +588,115 @@ impl Sizable for Tab {
|
||||
}
|
||||
|
||||
impl RenderOnce for Tab {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (text_color, hover_text_color, bg_color, border_color) =
|
||||
match (self.selected, self.disabled) {
|
||||
(true, false) => (
|
||||
cx.theme().tab_active_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().tab_active_background,
|
||||
cx.theme().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_transparent,
|
||||
),
|
||||
(true, true) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_disabled,
|
||||
),
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let mut tab_style = if self.selected {
|
||||
self.variant.selected(cx)
|
||||
} else {
|
||||
self.variant.normal(cx)
|
||||
};
|
||||
|
||||
let mut hover_style = self.variant.hovered(self.selected, cx);
|
||||
|
||||
if self.disabled {
|
||||
tab_style = self.variant.disabled(self.selected, cx);
|
||||
hover_style = self.variant.disabled(self.selected, cx);
|
||||
}
|
||||
|
||||
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
|
||||
|
||||
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
|
||||
tab_style.borders.left = px(0.);
|
||||
hover_style.borders.left = px(0.);
|
||||
}
|
||||
|
||||
let radius = self.variant.radius(self.size, cx);
|
||||
let inner_radius = self.variant.inner_radius(self.size, cx);
|
||||
let inner_paddings = self.variant.inner_paddings(self.size);
|
||||
let inner_margins = self.variant.inner_margins(self.size);
|
||||
let inner_height = self.variant.inner_height(self.size);
|
||||
let height = self.variant.height(self.size);
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.px_4()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.flex_shrink_0()
|
||||
.cursor_pointer()
|
||||
.h(height)
|
||||
.overflow_hidden()
|
||||
.text_xs()
|
||||
.text_ellipsis()
|
||||
.text_color(text_color)
|
||||
.bg(bg_color)
|
||||
.border_l(px(1.))
|
||||
.border_r(px(1.))
|
||||
.border_color(border_color)
|
||||
.text_color(tab_style.fg)
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs(),
|
||||
Size::Large => this.text_base(),
|
||||
_ => this.text_sm(),
|
||||
})
|
||||
.bg(tab_style.bg)
|
||||
.border_l(tab_style.borders.left)
|
||||
.border_r(tab_style.borders.right)
|
||||
.border_t(tab_style.borders.top)
|
||||
.border_b(tab_style.borders.bottom)
|
||||
.border_color(tab_style.border_color)
|
||||
.rounded(radius)
|
||||
.when(!self.selected && !self.disabled, |this| {
|
||||
this.hover(|this| this.text_color(hover_text_color))
|
||||
this.hover(|this| {
|
||||
this.text_color(hover_style.fg)
|
||||
.bg(hover_style.bg)
|
||||
.border_l(hover_style.borders.left)
|
||||
.border_r(hover_style.borders.right)
|
||||
.border_t(hover_style.borders.top)
|
||||
.border_b(hover_style.borders.bottom)
|
||||
.border_color(hover_style.border_color)
|
||||
.rounded(radius)
|
||||
})
|
||||
.when_some(self.prefix, |this, prefix| {
|
||||
this.child(prefix).text_color(text_color)
|
||||
})
|
||||
.when_some(self.label, |this, label| this.child(label))
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.h(inner_height)
|
||||
.line_height(relative(1.))
|
||||
.whitespace_nowrap()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.margins(inner_margins)
|
||||
.flex_shrink_0()
|
||||
.map(|this| match self.icon {
|
||||
Some(icon) => {
|
||||
this.w(inner_height * 1.25)
|
||||
.child(icon.map(|this| match self.size {
|
||||
Size::XSmall => this.size_2p5(),
|
||||
Size::Small => this.size_3p5(),
|
||||
Size::Large => this.size_4(),
|
||||
_ => this.size_4(),
|
||||
}))
|
||||
}
|
||||
None => this
|
||||
.paddings(inner_paddings)
|
||||
.map(|this| match self.label {
|
||||
Some(label) => this.child(label),
|
||||
None => this,
|
||||
})
|
||||
.children(self.children),
|
||||
})
|
||||
.bg(tab_style.inner_bg)
|
||||
.rounded(inner_radius)
|
||||
.when(tab_style.shadow, |this| this.shadow_xs())
|
||||
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().pr_2().child(suffix))
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
// Stop propagation behavior, for works on TitleBar.
|
||||
// https://github.com/longbridge/gpui-component/issues/1836
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,92 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use gpui::Pixels;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||
AnyElement, App, Corner, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Sizable, Size, StyledExt};
|
||||
use super::{Tab, TabVariant};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::menu::{DropdownMenu as _, PopupMenuItem};
|
||||
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A TabBar element that contains multiple [`Tab`] items.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Div,
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
scroll_handle: Option<ScrollHandle>,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
children: SmallVec<[Tab; 2]>,
|
||||
last_empty_space: AnyElement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
selected_index: Option<usize>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
menu: bool,
|
||||
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new() -> Self {
|
||||
/// Create a new TabBar.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: h_flex().px(px(-1.)),
|
||||
base: div().id(id).px(px(-1.)),
|
||||
style: StyleRefinement::default(),
|
||||
scroll_handle: None,
|
||||
children: SmallVec::new(),
|
||||
scroll_handle: None,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
last_empty_space: div().w_3().into_any_element(),
|
||||
selected_index: None,
|
||||
on_click: None,
|
||||
menu: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Tab variant, all children will inherit the variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Pill, all children will inherit the variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Outline, all children will inherit the variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Segmented, all children will inherit the variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Underline, all children will inherit the variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to show the menu button when tabs overflow, default is false.
|
||||
pub fn menu(mut self, menu: bool) -> Self {
|
||||
self.menu = menu;
|
||||
self
|
||||
}
|
||||
|
||||
/// Track the scroll of the TabBar.
|
||||
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle.clone());
|
||||
@@ -54,27 +105,39 @@ impl TabBar {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add children of the TabBar, all children will inherit the variant.
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||
self.children.extend(children.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add child of the TabBar, tab will inherit the variant.
|
||||
pub fn child(mut self, child: impl Into<Tab>) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the selected index of the TabBar.
|
||||
pub fn selected_index(mut self, index: usize) -> Self {
|
||||
self.selected_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last empty space element of the TabBar.
|
||||
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
self.last_empty_space = last_empty_space.into_any_element();
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn height(window: &mut Window) -> Pixels {
|
||||
(1.75 * window.rem_size()).max(px(36.))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TabBar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TabBar {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
|
||||
///
|
||||
/// When this is set, the children's on_click will be ignored.
|
||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: Fn(&usize, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,15 +155,69 @@ impl Sizable for TabBar {
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let default_gap = match self.size {
|
||||
Size::Small | Size::XSmall => px(8.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
let (bg, paddings, gap) = match self.variant {
|
||||
TabVariant::Tab => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(cx.theme().tab_background, padding, px(0.))
|
||||
}
|
||||
TabVariant::Outline => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, default_gap)
|
||||
}
|
||||
TabVariant::Pill => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, px(4.))
|
||||
}
|
||||
TabVariant::Segmented => {
|
||||
let padding_x = match self.size {
|
||||
Size::XSmall => px(2.),
|
||||
Size::Small => px(3.),
|
||||
_ => px(4.),
|
||||
};
|
||||
let padding = Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(cx.theme().tab_background, padding, px(2.))
|
||||
}
|
||||
TabVariant::Underline => {
|
||||
// This gap is same as the tab inner_paddings
|
||||
let gap = match self.size {
|
||||
Size::XSmall => px(10.),
|
||||
Size::Small => px(12.),
|
||||
Size::Large => px(20.),
|
||||
_ => px(16.),
|
||||
};
|
||||
|
||||
(gpui::transparent_black(), Edges::all(px(0.)), gap)
|
||||
}
|
||||
};
|
||||
|
||||
let mut item_labels = Vec::new();
|
||||
let selected_index = self.selected_index;
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
self.base
|
||||
.group("tab-bar")
|
||||
.relative()
|
||||
.refine_style(&self.style)
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
.flex()
|
||||
.items_center()
|
||||
.bg(bg)
|
||||
.text_color(cx.theme().tab_foreground)
|
||||
.when(
|
||||
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.id("border-bottom")
|
||||
.id("border-b")
|
||||
.absolute()
|
||||
.left_0()
|
||||
.bottom_0()
|
||||
@@ -108,21 +225,66 @@ impl RenderOnce for TabBar {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border),
|
||||
)
|
||||
.text_color(cx.theme().text)
|
||||
},
|
||||
)
|
||||
.rounded(self.variant.tab_bar_radius(self.size, cx))
|
||||
.paddings(paddings)
|
||||
.refine_style(&self.style)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tabs")
|
||||
.flex_grow()
|
||||
.flex_1()
|
||||
.overflow_x_scroll()
|
||||
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||
this.track_scroll(&scroll_handle)
|
||||
})
|
||||
.children(self.children)
|
||||
.when(self.suffix.is_some(), |this| {
|
||||
.gap(gap)
|
||||
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
|
||||
item_labels.push((child.label.clone(), child.disabled));
|
||||
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
|
||||
child
|
||||
.ix(ix)
|
||||
.tab_bar_prefix(tab_bar_prefix)
|
||||
.with_variant(self.variant)
|
||||
.with_size(self.size)
|
||||
.when_some(self.selected_index, |this, selected_ix| {
|
||||
this.selected(selected_ix == ix)
|
||||
})
|
||||
.when_some(self.on_click.clone(), move |this, on_click| {
|
||||
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
|
||||
})
|
||||
}))
|
||||
.when(self.suffix.is_some() || self.menu, |this| {
|
||||
this.child(self.last_empty_space)
|
||||
}),
|
||||
)
|
||||
.when(self.menu, |this| {
|
||||
this.child(
|
||||
Button::new("more")
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.icon(IconName::ChevronDown)
|
||||
.dropdown_menu(move |mut this, _, _| {
|
||||
this = this.scrollable(true);
|
||||
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
|
||||
this = this.item(
|
||||
PopupMenuItem::new(label.clone().unwrap_or_default())
|
||||
.checked(selected_index == Some(ix))
|
||||
.disabled(*disabled)
|
||||
.when_some(on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |_, window, cx| {
|
||||
on_click(&ix, window, cx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
.anchor(Corner::TopRight),
|
||||
)
|
||||
})
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Entity, SharedString, Window};
|
||||
use gpui::{App, ElementId, Entity, Window};
|
||||
|
||||
use crate::Root;
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
use crate::notification::Notification;
|
||||
use crate::Root;
|
||||
|
||||
/// Extension trait for [`Window`] to add modal, notification .. functionality.
|
||||
pub trait WindowExtension: Sized {
|
||||
@@ -31,10 +31,15 @@ pub trait WindowExtension: Sized {
|
||||
where
|
||||
T: Into<Notification>;
|
||||
|
||||
/// Clears a notification by its ID.
|
||||
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
|
||||
where
|
||||
T: Into<SharedString>;
|
||||
/// Clear the unique notification.
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App);
|
||||
|
||||
/// Clear the unique notification with the given id.
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Clear all notifications
|
||||
fn clear_notifications(&mut self, cx: &mut App);
|
||||
@@ -88,13 +93,21 @@ impl WindowExtension for Window {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
let id = id.into();
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.clear_notification(id, window, cx);
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification::<T>(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let key: ElementId = key.into();
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification_by_id::<T>(key, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user