Merge branch 'main' into focus-handlers-on-draw
This commit is contained in:
commit
4be84f3db0
108 changed files with 4470 additions and 3168 deletions
167
Cargo.lock
generated
167
Cargo.lock
generated
|
@ -182,7 +182,7 @@ dependencies = [
|
|||
"alacritty_config",
|
||||
"alacritty_config_derive",
|
||||
"base64 0.13.1",
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -200,7 +200,7 @@ dependencies = [
|
|||
"toml 0.7.8",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -293,7 +293,7 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -303,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -580,7 +580,7 @@ dependencies = [
|
|||
"futures-lite",
|
||||
"rustix 0.37.23",
|
||||
"signal-hook",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1025,9 +1025,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -1753,7 +1753,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.30.1"
|
||||
version = "0.31.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -1949,6 +1949,7 @@ name = "collab_ui2"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update2",
|
||||
"call2",
|
||||
"channel2",
|
||||
"client2",
|
||||
|
@ -1957,6 +1958,7 @@ dependencies = [
|
|||
"db2",
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"feedback2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
|
@ -1978,10 +1980,12 @@ dependencies = [
|
|||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"theme_selector2",
|
||||
"time",
|
||||
"tree-sitter-markdown",
|
||||
"ui2",
|
||||
"util",
|
||||
"vcs_menu2",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
@ -2084,6 +2088,19 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-cstr"
|
||||
version = "0.3.0"
|
||||
|
@ -2571,7 +2588,7 @@ dependencies = [
|
|||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2780,6 +2797,20 @@ dependencies = [
|
|||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||
dependencies = [
|
||||
"console",
|
||||
"fuzzy-matcher",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
|
@ -3025,6 +3056,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
|
@ -3103,7 +3140,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
|
|||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3134,7 +3171,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
|
|||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"home",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3246,6 +3283,7 @@ name = "feedback2"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.1",
|
||||
"client2",
|
||||
"db2",
|
||||
"editor2",
|
||||
|
@ -3265,6 +3303,7 @@ dependencies = [
|
|||
"serde_derive",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"theme2",
|
||||
"tree-sitter-markdown",
|
||||
|
@ -3341,7 +3380,7 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.3.5",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3720,6 +3759,15 @@ dependencies = [
|
|||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy2"
|
||||
version = "0.1.0"
|
||||
|
@ -3982,7 +4030,7 @@ dependencies = [
|
|||
"async-task",
|
||||
"backtrace",
|
||||
"bindgen 0.65.1",
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"block",
|
||||
"cbindgen",
|
||||
"cocoa",
|
||||
|
@ -4241,7 +4289,7 @@ version = "0.5.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4518,7 +4566,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
|||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4575,7 +4623,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
|||
dependencies = [
|
||||
"hermit-abi 0.3.3",
|
||||
"rustix 0.38.14",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4998,7 +5046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5491,7 +5539,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6151,7 +6199,7 @@ version = "0.10.57"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
@ -6510,6 +6558,7 @@ dependencies = [
|
|||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6671,7 +6720,7 @@ dependencies = [
|
|||
"libc",
|
||||
"log",
|
||||
"pin-project-lite 0.2.13",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -7019,6 +7068,29 @@ dependencies = [
|
|||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_symbols2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lsp2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"project2",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.3"
|
||||
|
@ -7847,7 +7919,7 @@ version = "0.29.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
|
@ -7954,7 +8026,7 @@ dependencies = [
|
|||
"io-lifetimes 1.0.11",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -7963,11 +8035,11 @@ version = "0.38.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"errno 0.3.3",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.7",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -8080,7 +8152,7 @@ version = "0.1.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -8680,6 +8752,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "2.1.2"
|
||||
|
@ -8879,7 +8957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -9060,7 +9138,7 @@ dependencies = [
|
|||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"chrono",
|
||||
|
@ -9107,7 +9185,7 @@ dependencies = [
|
|||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.0",
|
||||
"bitflags 2.4.1",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
|
@ -9195,6 +9273,7 @@ dependencies = [
|
|||
"backtrace-on-stack-overflow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"dialoguer",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
|
@ -9495,7 +9574,7 @@ dependencies = [
|
|||
"fastrand 2.0.0",
|
||||
"redox_syscall 0.3.5",
|
||||
"rustix 0.38.14",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -9934,7 +10013,7 @@ dependencies = [
|
|||
"signal-hook-registry",
|
||||
"socket2 0.5.4",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -10848,6 +10927,20 @@ dependencies = [
|
|||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcs_menu2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fs2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"picker2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
@ -11550,6 +11643,15 @@ dependencies = [
|
|||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
@ -11689,7 +11791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -11882,7 +11984,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.117.0"
|
||||
version = "0.118.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
|
@ -12081,6 +12183,7 @@ dependencies = [
|
|||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"project_symbols2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
|
|
|
@ -90,6 +90,7 @@ members = [
|
|||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/project_symbols2",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
|
@ -122,6 +123,7 @@ members = [
|
|||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/vcs_menu2",
|
||||
"crates/workspace2",
|
||||
"crates/welcome",
|
||||
"crates/welcome2",
|
||||
|
|
|
@ -6,15 +6,17 @@ use db::kvp::KEY_VALUE_STORE;
|
|||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
|
||||
ViewContext, VisualContext,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::{AppCommitSha, ReleaseChannel};
|
||||
|
@ -24,16 +26,7 @@ use workspace::Workspace;
|
|||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
//todo!(remove CheckThatAutoUpdaterWorks)
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
CheckThatAutoUpdaterWorks
|
||||
]
|
||||
);
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
|
@ -90,7 +83,10 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
|||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
|
||||
workspace.register_action(|_, action, cx| view_release_notes(action, cx));
|
||||
|
||||
// @nate - code to trigger update notification on launch
|
||||
// todo!("remove this when Nate is done")
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
|
||||
// });
|
||||
|
@ -119,13 +115,10 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
|||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
//todo!(action)
|
||||
// cx.add_global_action(view_release_notes);
|
||||
// cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
|
|
|
@ -2,6 +2,7 @@ use gpui::{
|
|||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
|
@ -18,6 +19,7 @@ impl Render for UpdateNotification {
|
|||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
v_stack()
|
||||
.on_action(cx.listener(UpdateNotification::dismiss))
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.child(
|
||||
|
@ -32,7 +34,7 @@ impl Render for UpdateNotification {
|
|||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
@ -50,7 +52,7 @@ impl UpdateNotification {
|
|||
Self { version }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
|
|||
http::{Request, StatusCode},
|
||||
};
|
||||
use futures::{
|
||||
future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
|
||||
TryStreamExt,
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
|
||||
|
@ -1020,91 +1020,116 @@ impl Client {
|
|||
) -> Task<Result<Credentials>> {
|
||||
let http = self.http.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string =
|
||||
String::try_from(public_key).expect("failed to serialize public key for auth");
|
||||
let background = cx.background_executor().clone();
|
||||
|
||||
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
|
||||
}
|
||||
let (open_url_tx, open_url_rx) = oneshot::channel::<String>();
|
||||
cx.update(|cx| {
|
||||
cx.spawn(move |cx| async move {
|
||||
let url = open_url_rx.await?;
|
||||
cx.update(|cx| cx.open_url(&url))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
let credentials = background
|
||||
.clone()
|
||||
.spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string = String::try_from(public_key)
|
||||
.expect("failed to serialize public key for auth");
|
||||
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server =
|
||||
tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
|
||||
cx.update(|cx| cx.open_url(&url))?;
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = cx
|
||||
.spawn(|_| async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
|
||||
open_url_tx.send(url).log_err();
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = background
|
||||
.spawn(async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id
|
||||
.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token.ok_or_else(|| {
|
||||
anyhow!("missing access_token parameter")
|
||||
})?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token
|
||||
.ok_or_else(|| anyhow!("missing access_token parameter"))?,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
cx.update(|cx| cx.activate(true))?;
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
Ok(credentials)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.30.1"
|
||||
version = "0.31.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -22,7 +22,7 @@ test-support = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
# auto_update = { path = "../auto_update" }
|
||||
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
call = { package = "call2", path = "../call2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
|
@ -32,7 +32,7 @@ collections = { path = "../collections" }
|
|||
# context_menu = { path = "../context_menu" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
feedback = { package = "feedback2", path = "../feedback2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
|
@ -46,8 +46,8 @@ rpc = { package ="rpc2", path = "../rpc2" }
|
|||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
# theme_selector = { path = "../theme_selector" }
|
||||
# vcs_menu = { path = "../vcs_menu" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
|
|
@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
|
|||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
|
||||
IconButton, Label, Tooltip,
|
||||
};
|
||||
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
|
@ -382,13 +379,18 @@ impl ChatPanel {
|
|||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_2()
|
||||
.w_8()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(render_remove(message_id_to_remove, cx)),
|
||||
.visible_on_hover("")
|
||||
.children(message_id_to_remove.map(|message_id| {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
@ -528,18 +530,6 @@ impl ChatPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
|
||||
if let Some(message_id) = message_id_to_remove {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ChatPanel {}
|
||||
|
||||
impl Render for ChatPanel {
|
||||
|
|
|
@ -175,12 +175,12 @@ use editor::Editor;
|
|||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
|
||||
size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
|
||||
EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement,
|
||||
Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render,
|
||||
RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
|
||||
serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
|
||||
DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
|
||||
InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Fs, Project};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
@ -402,7 +402,7 @@ impl CollabPanel {
|
|||
|
||||
let filter_editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text("Filter channels, contacts", cx);
|
||||
editor.set_placeholder_text("Filter...", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
|
@ -1156,25 +1156,21 @@ impl CollabPanel {
|
|||
let tooltip = format!("Follow {}", user.github_login);
|
||||
|
||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||
.left_child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.child(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", Icon::ArrowRight)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}),
|
||||
)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", Icon::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
.when_some(peer_id, |this, peer_id| {
|
||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
|
@ -1212,8 +1208,12 @@ impl CollabPanel {
|
|||
.detach_and_log_err(cx);
|
||||
});
|
||||
}))
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Folder))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Folder)),
|
||||
)
|
||||
.child(Label::new(project_name.clone()))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||
|
||||
|
@ -1305,8 +1305,12 @@ impl CollabPanel {
|
|||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||
|
||||
ListItem::new(("screen", id))
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Screen))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Screen)),
|
||||
)
|
||||
.child(Label::new("Screen"))
|
||||
.when_some(peer_id, |this, _| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
|
@ -1372,9 +1376,13 @@ impl CollabPanel {
|
|||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::File))
|
||||
.child(Label::new("notes"))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::File)),
|
||||
)
|
||||
.child(div().h_7().w_full().child(Label::new("notes")))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
}
|
||||
|
||||
|
@ -1387,8 +1395,12 @@ impl CollabPanel {
|
|||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(true, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles)),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
}
|
||||
|
@ -2130,7 +2142,7 @@ impl CollabPanel {
|
|||
}
|
||||
|
||||
fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
v_stack().child(
|
||||
v_stack().border_1().border_color(gpui::red()).child(
|
||||
Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
let client = this.client.clone();
|
||||
|
@ -2149,11 +2161,6 @@ impl CollabPanel {
|
|||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
v_stack()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.size_full()
|
||||
|
@ -2223,6 +2230,14 @@ impl CollabPanel {
|
|||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div().p_2().child(
|
||||
div()
|
||||
.border_primary(cx)
|
||||
.border_t()
|
||||
.child(self.filter_editor.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
|
@ -2275,21 +2290,32 @@ impl CollabPanel {
|
|||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
IconButton::new("channel-link", Icon::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
.visible_on_hover("section-header")
|
||||
.on_click(move |_, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
IconButton::new("add-contact", Icon::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx)),
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(gpui::red())
|
||||
.child(
|
||||
IconButton::new("add-contact", Icon::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx)),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
Section::Channels => Some(
|
||||
IconButton::new("add-channel", Icon::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Create a channel", cx)),
|
||||
.tooltip(|cx| Tooltip::text("Create a channel", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
|
@ -2302,27 +2328,20 @@ impl CollabPanel {
|
|||
| Section::Offline => true,
|
||||
};
|
||||
|
||||
h_stack()
|
||||
let mut row = h_stack()
|
||||
.w_full()
|
||||
.map(|el| {
|
||||
if can_collapse {
|
||||
el.child(
|
||||
ListItem::new(text.clone())
|
||||
.child(div().w_full().child(Label::new(text)))
|
||||
.selected(is_selected)
|
||||
.toggle(Some(!is_collapsed))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx)
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
el.child(
|
||||
ListHeader::new(text)
|
||||
.when_some(button, |el, button| el.meta(button))
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
})
|
||||
.group("section-header")
|
||||
.child(
|
||||
ListHeader::new(text)
|
||||
.toggle(if can_collapse {
|
||||
Some(!is_collapsed)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
.when(section == Section::Channels, |el| {
|
||||
el.drag_over::<DraggedChannelView>(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
|
@ -2336,7 +2355,13 @@ impl CollabPanel {
|
|||
.detach_and_log_err(cx)
|
||||
},
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
if section == Section::Offline {
|
||||
row = div().border_1().border_color(gpui::red()).child(row);
|
||||
}
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
|
@ -2363,25 +2388,20 @@ impl CollabPanel {
|
|||
})
|
||||
.when(!calling, |el| {
|
||||
el.child(
|
||||
div()
|
||||
.id("remove_contact")
|
||||
.invisible()
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new("remove_contact", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||
.on_click(cx.listener({
|
||||
let github_login = github_login.clone();
|
||||
move |this, _, cx| {
|
||||
this.remove_contact(user_id, &github_login, cx);
|
||||
}
|
||||
})),
|
||||
),
|
||||
IconButton::new("remove_contact", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.visible_on_hover("")
|
||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||
.on_click(cx.listener({
|
||||
let github_login = github_login.clone();
|
||||
move |this, _, cx| {
|
||||
this.remove_contact(user_id, &github_login, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.left_child(
|
||||
.start_slot(
|
||||
// todo!() handle contacts with no avatar
|
||||
Avatar::new(contact.user.avatar_uri.clone())
|
||||
.availability_indicator(if online { Some(!busy) } else { None }),
|
||||
|
@ -2460,7 +2480,7 @@ impl CollabPanel {
|
|||
.child(Label::new(github_login.clone()))
|
||||
.child(h_stack().children(controls)),
|
||||
)
|
||||
.left_avatar(user.avatar_uri.clone())
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
}
|
||||
|
||||
fn render_contact_placeholder(
|
||||
|
@ -2541,12 +2561,13 @@ impl CollabPanel {
|
|||
div()
|
||||
.id(channel_id as usize)
|
||||
.group("")
|
||||
.on_drag({
|
||||
let channel = channel.clone();
|
||||
move |cx| {
|
||||
let channel = channel.clone();
|
||||
cx.build_view(|cx| DraggedChannelView { channel, width })
|
||||
}
|
||||
.flex()
|
||||
.w_full()
|
||||
.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.build_view(|cx| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
})
|
||||
})
|
||||
.drag_over::<DraggedChannelView>(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
|
@ -2566,67 +2587,10 @@ impl CollabPanel {
|
|||
)
|
||||
.child(
|
||||
ListItem::new(channel_id as usize)
|
||||
.indent_level(depth)
|
||||
// Offset the indent depth by one to give us room to show the disclosure.
|
||||
.indent_level(depth + 1)
|
||||
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
||||
.left_icon(if is_public { Icon::Public } else { Icon::Hash })
|
||||
.selected(is_selected || is_active)
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.id(channel_id as usize)
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
div()
|
||||
.id("channel_chat")
|
||||
.when(!has_messages_notification, |el| el.invisible())
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new(
|
||||
"channel_chat",
|
||||
Icon::MessageBubbles,
|
||||
)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel chat", cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("channel_notes")
|
||||
.when(!has_notes_notification, |el| el.invisible())
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new("channel_notes", Icon::File)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel notes", cx)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toggle(disclosed)
|
||||
.on_toggle(
|
||||
cx.listener(move |this, _, cx| {
|
||||
|
@ -2646,7 +2610,49 @@ impl CollabPanel {
|
|||
move |this, event: &MouseDownEvent, cx| {
|
||||
this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
|
||||
},
|
||||
)),
|
||||
))
|
||||
.start_slot(
|
||||
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.id(channel_id as usize)
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||
)
|
||||
.end_slot(
|
||||
h_stack()
|
||||
.child(
|
||||
IconButton::new("channel_chat", Icon::MessageBubbles)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.when(!has_messages_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", Icon::File)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.when(!has_notes_notification, |this| this.visible_on_hover(""))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
||||
|
||||
|
@ -2962,7 +2968,11 @@ impl CollabPanel {
|
|||
let item = ListItem::new("channel-editor")
|
||||
.inset(false)
|
||||
.indent_level(depth)
|
||||
.left_icon(Icon::Hash);
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Hash)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
|
||||
if let Some(pending_name) = self
|
||||
.channel_editing_state
|
||||
|
@ -2994,7 +3004,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
|
|||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
|
@ -3002,18 +3012,12 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
|
|||
if is_last { start_y } else { bounds.bottom() },
|
||||
),
|
||||
),
|
||||
Default::default(),
|
||||
color,
|
||||
Default::default(),
|
||||
Hsla::transparent_black(),
|
||||
);
|
||||
cx.paint_quad(
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
Default::default(),
|
||||
color,
|
||||
Default::default(),
|
||||
Hsla::transparent_black(),
|
||||
);
|
||||
));
|
||||
})
|
||||
.w(width)
|
||||
.h(line_height)
|
||||
|
|
|
@ -11,14 +11,8 @@ use ui::{prelude::*, Avatar};
|
|||
use util::{ResultExt as _, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
//cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: View<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
|
@ -31,16 +25,12 @@ impl ContactFinder {
|
|||
};
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
Self { picker }
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
// todo!()
|
||||
// picker.set_query(query, cx);
|
||||
picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -62,32 +52,9 @@ impl Render for ContactFinder {
|
|||
.w(rems(34.))
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
|
||||
type Element = Div;
|
||||
}
|
||||
|
||||
// impl Modal for ContactFinder {
|
||||
// fn has_focus(&self) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
parent: WeakView<ContactFinder>,
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
|
@ -161,7 +128,6 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
|
@ -191,6 +157,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
.child(Label::new(user.github_login.clone()))
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
)
|
||||
// todo!()
|
||||
// Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
use crate::face_pile::FacePile;
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
|
||||
use gpui::{
|
||||
actions, canvas, div, point, px, rems, AppContext, Div, Element, Hsla, InteractiveElement,
|
||||
IntoElement, Model, ParentElement, Path, Render, Stateful, StatefulInteractiveElement, Styled,
|
||||
Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
|
||||
actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent,
|
||||
Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
|
||||
Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
|
||||
VisualContext, WeakView, WindowBounds,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
use std::sync::Arc;
|
||||
use theme::{ActiveTheme, PlayerColors};
|
||||
use ui::{
|
||||
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
|
||||
IconButton, IconElement, KeyBinding, Tooltip,
|
||||
IconButton, IconElement, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB};
|
||||
|
||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
||||
|
@ -48,9 +52,8 @@ pub struct CollabTitlebarItem {
|
|||
user_store: Model<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakView<Workspace>,
|
||||
//branch_popover: Option<ViewHandle<BranchList>>,
|
||||
//project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||
//user_menu: ViewHandle<ContextMenu>,
|
||||
branch_popover: Option<View<BranchList>>,
|
||||
project_popover: Option<recent_projects::RecentProjects>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
|
@ -230,56 +233,17 @@ impl Render for CollabTitlebarItem {
|
|||
}),
|
||||
)
|
||||
})
|
||||
.child(h_stack().px_1p5().map(|this| {
|
||||
if let Some(user) = current_user {
|
||||
// TODO: Finish implementing user menu popover
|
||||
//
|
||||
this.child(
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| menu.header("ADADA"))
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Toggle User Menu", cx)
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight),
|
||||
)
|
||||
// this.child(
|
||||
// ButtonLike::new("user-menu")
|
||||
// .child(
|
||||
// h_stack().gap_0p5().child(Avatar::data(avatar)).child(
|
||||
// IconElement::new(Icon::ChevronDown).color(Color::Muted),
|
||||
// ),
|
||||
// )
|
||||
// .style(ButtonStyle::Subtle)
|
||||
// .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
// )
|
||||
.map(|el| {
|
||||
let status = self.client.status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
el.child(self.render_user_menu_button(cx))
|
||||
} else {
|
||||
this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.notify_async_err(&mut cx);
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
el.children(self.render_connection_status(status, cx))
|
||||
.child(self.render_sign_in_button(cx))
|
||||
.child(self.render_user_menu_button(cx))
|
||||
}
|
||||
})),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -321,14 +285,8 @@ impl CollabTitlebarItem {
|
|||
project,
|
||||
user_store,
|
||||
client,
|
||||
// user_menu: cx.add_view(|cx| {
|
||||
// let view_id = cx.view_id();
|
||||
// let mut menu = ContextMenu::new(view_id, cx);
|
||||
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||
// menu
|
||||
// }),
|
||||
// branch_popover: None,
|
||||
// project_popover: None,
|
||||
branch_popover: None,
|
||||
project_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
@ -366,11 +324,27 @@ impl CollabTitlebarItem {
|
|||
|
||||
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||
|
||||
div().border().border_color(gpui::red()).child(
|
||||
Button::new("project_name_trigger", name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
)
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(
|
||||
Button::new("project_name_trigger", name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx))
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_project_menu(&ToggleProjectMenu, cx);
|
||||
})),
|
||||
)
|
||||
.children(self.project_popover.as_ref().map(|popover| {
|
||||
overlay().child(
|
||||
div()
|
||||
.min_w_56()
|
||||
.on_mouse_down_out(cx.listener_for(&popover.picker, |picker, _, cx| {
|
||||
picker.cancel(&Default::default(), cx)
|
||||
}))
|
||||
.child(popover.picker.clone()),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||
|
@ -390,23 +364,25 @@ impl CollabTitlebarItem {
|
|||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||
|
||||
Some(
|
||||
div().border().border_color(gpui::red()).child(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
cx.build_view(|_| {
|
||||
Tooltip::new("Recent Branches")
|
||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
||||
"cmd-b",
|
||||
// todo!() Replace with real action.
|
||||
gpui::NoAction,
|
||||
None,
|
||||
)))
|
||||
.meta("Local branches only")
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.child(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
.on_click(
|
||||
cx.listener(|this, _, cx| this.toggle_vcs_menu(&ToggleVcsMenu, cx)),
|
||||
),
|
||||
)
|
||||
.children(self.render_branches_popover_host()),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -485,318 +461,177 @@ impl CollabTitlebarItem {
|
|||
.log_err();
|
||||
}
|
||||
|
||||
// pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
// self.user_menu.update(cx, |user_menu, cx| {
|
||||
// let items = if let Some(_) = self.user_store.read(cx).current_user() {
|
||||
// vec![
|
||||
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
// ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
// ContextMenuItem::separator(),
|
||||
// ContextMenuItem::action(
|
||||
// "Share Feedback",
|
||||
// feedback::feedback_editor::GiveFeedback,
|
||||
// ),
|
||||
// ContextMenuItem::action("Sign Out", SignOut),
|
||||
// ]
|
||||
// } else {
|
||||
// vec![
|
||||
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
// ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
// ContextMenuItem::separator(),
|
||||
// ContextMenuItem::action(
|
||||
// "Share Feedback",
|
||||
// feedback::feedback_editor::GiveFeedback,
|
||||
// ),
|
||||
// ]
|
||||
// };
|
||||
// user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
// });
|
||||
// }
|
||||
fn render_branches_popover_host<'a>(&'a self) -> Option<AnyElement> {
|
||||
self.branch_popover.as_ref().map(|child| {
|
||||
overlay()
|
||||
.child(div().min_w_64().child(child.clone()))
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
// fn render_branches_popover_host<'a>(
|
||||
// &'a self,
|
||||
// _theme: &'a theme::Titlebar,
|
||||
// cx: &'a mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// self.branch_popover.as_ref().map(|child| {
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let child = ChildView::new(child, cx);
|
||||
// let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
|
||||
// child
|
||||
// .flex(1., true)
|
||||
// .contained()
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.menu.width)
|
||||
// .with_height(theme.titlebar.menu.height)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, |_, _, _| {})
|
||||
// .on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
// this.branch_popover.take();
|
||||
// cx.emit(());
|
||||
// cx.notify();
|
||||
// })
|
||||
// .contained()
|
||||
// .into_any();
|
||||
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.branch_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let Some(view) = build_branch_list(workspace, cx).log_err() else {
|
||||
return;
|
||||
};
|
||||
cx.subscribe(&view, |this, _, _, cx| {
|
||||
this.branch_popover = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.project_popover.take();
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
self.branch_popover = Some(view);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay::new(child)
|
||||
// .with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
// .with_anchor_corner(AnchorCorner::TopLeft)
|
||||
// .with_z_index(999)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .left()
|
||||
// .into_any()
|
||||
// })
|
||||
// }
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// fn render_project_popover_host<'a>(
|
||||
// &'a self,
|
||||
// _theme: &'a theme::Titlebar,
|
||||
// cx: &'a mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// self.project_popover.as_ref().map(|child| {
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let child = ChildView::new(child, cx);
|
||||
// let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
|
||||
// child
|
||||
// .flex(1., true)
|
||||
// .contained()
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.menu.width)
|
||||
// .with_height(theme.titlebar.menu.height)
|
||||
// })
|
||||
// .on_click(MouseButton::Left, |_, _, _| {})
|
||||
// .on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
// this.project_popover.take();
|
||||
// cx.emit(());
|
||||
// cx.notify();
|
||||
// })
|
||||
// .into_any();
|
||||
pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
if self.project_popover.take().is_none() {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let workspaces = WORKSPACE_DB
|
||||
.recent_workspaces_on_disk()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(_, location)| location)
|
||||
.collect();
|
||||
|
||||
// Overlay::new(child)
|
||||
// .with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
// .with_anchor_corner(AnchorCorner::TopLeft)
|
||||
// .with_z_index(999)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .left()
|
||||
// .into_any()
|
||||
// })
|
||||
// }
|
||||
let workspace = workspace.clone();
|
||||
this.update(&mut cx, move |this, cx| {
|
||||
let view = RecentProjects::open_popover(workspace, workspaces, cx);
|
||||
|
||||
// pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
// if self.branch_popover.take().is_none() {
|
||||
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
// let Some(view) =
|
||||
// cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
// cx.subscribe(&view, |this, _, event, cx| {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => {
|
||||
// this.branch_popover = None;
|
||||
// }
|
||||
// }
|
||||
cx.subscribe(&view.picker, |this, _, _: &DismissEvent, cx| {
|
||||
this.project_popover = None;
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
// todo!()
|
||||
//this.branch_popover.take();
|
||||
this.project_popover = Some(view);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// cx.notify();
|
||||
// })
|
||||
// .detach();
|
||||
// self.project_popover.take();
|
||||
// cx.focus(&view);
|
||||
// self.branch_popover = Some(view);
|
||||
// }
|
||||
// }
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
match status {
|
||||
client::Status::ConnectionError
|
||||
| client::Status::ConnectionLost
|
||||
| client::Status::Reauthenticating { .. }
|
||||
| client::Status::Reconnecting { .. }
|
||||
| client::Status::ReconnectionError { .. } => Some(
|
||||
div()
|
||||
.id("disconnected")
|
||||
.bg(gpui::red()) // todo!() @nate
|
||||
.child(IconElement::new(Icon::Disconnected))
|
||||
.tooltip(|cx| Tooltip::text("Disconnected", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
"Please update Zed to Collaborate"
|
||||
}
|
||||
};
|
||||
|
||||
// cx.notify();
|
||||
// }
|
||||
Some(
|
||||
div()
|
||||
.bg(gpui::red()) // todo!() @nate
|
||||
.child(Button::new("connection-status", label).on_click(|_, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto_update::check(&Default::default(), cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
|
||||
// let workspace = self.workspace.clone();
|
||||
// if self.project_popover.take().is_none() {
|
||||
// cx.spawn(|this, mut cx| async move {
|
||||
// let workspaces = WORKSPACE_DB
|
||||
// .recent_workspaces_on_disk()
|
||||
// .await
|
||||
// .unwrap_or_default()
|
||||
// .into_iter()
|
||||
// .map(|(_, location)| location)
|
||||
// .collect();
|
||||
pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
|
||||
let client = self.client.clone();
|
||||
Button::new("sign_in", "Sign in").on_click(move |_, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.notify_async_err(&mut cx);
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
|
||||
// let workspace = workspace.clone();
|
||||
// this.update(&mut cx, move |this, cx| {
|
||||
// let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
|
||||
|
||||
// cx.subscribe(&view, |this, _, event, cx| {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => {
|
||||
// this.project_popover = None;
|
||||
// }
|
||||
// }
|
||||
|
||||
// cx.notify();
|
||||
// })
|
||||
// .detach();
|
||||
// cx.focus(&view);
|
||||
// this.branch_popover.take();
|
||||
// this.project_popover = Some(view);
|
||||
// cx.notify();
|
||||
// })
|
||||
// .log_err();
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn render_user_menu_button(
|
||||
// &self,
|
||||
// theme: &Theme,
|
||||
// avatar: Option<Arc<ImageData>>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let tooltip = theme.tooltip.clone();
|
||||
// let user_menu_button_style = if avatar.is_some() {
|
||||
// &theme.titlebar.user_menu.user_menu_button_online
|
||||
// } else {
|
||||
// &theme.titlebar.user_menu.user_menu_button_offline
|
||||
// };
|
||||
|
||||
// let avatar_style = &user_menu_button_style.avatar;
|
||||
// Stack::new()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
|
||||
// let style = user_menu_button_style
|
||||
// .user_menu
|
||||
// .inactive_state()
|
||||
// .style_for(state);
|
||||
|
||||
// let mut dropdown = Flex::row().align_children_center();
|
||||
|
||||
// if let Some(avatar_img) = avatar {
|
||||
// dropdown = dropdown.with_child(Self::render_face(
|
||||
// avatar_img,
|
||||
// *avatar_style,
|
||||
// Color::transparent_black(),
|
||||
// None,
|
||||
// ));
|
||||
// };
|
||||
|
||||
// dropdown
|
||||
// .with_child(
|
||||
// Svg::new("icons/caret_down.svg")
|
||||
// .with_color(user_menu_button_style.icon.color)
|
||||
// .constrained()
|
||||
// .with_width(user_menu_button_style.icon.width)
|
||||
// .contained()
|
||||
// .into_any(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(style.width)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .into_any()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_down(MouseButton::Left, move |_, this, cx| {
|
||||
// this.user_menu.update(cx, |menu, _| menu.delay_cancel());
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.toggle_user_menu(&Default::default(), cx)
|
||||
// })
|
||||
// .with_tooltip::<ToggleUserMenu>(
|
||||
// 0,
|
||||
// "Toggle User Menu".to_owned(),
|
||||
// Some(Box::new(ToggleUserMenu)),
|
||||
// tooltip,
|
||||
// cx,
|
||||
// )
|
||||
// .contained(),
|
||||
// )
|
||||
// .with_child(
|
||||
// ChildView::new(&self.user_menu, cx)
|
||||
// .aligned()
|
||||
// .bottom()
|
||||
// .right(),
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let titlebar = &theme.titlebar;
|
||||
// MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
|
||||
// let style = titlebar.sign_in_button.inactive_state().style_for(state);
|
||||
// Label::new("Sign In", style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// let client = this.client.clone();
|
||||
// cx.app_context()
|
||||
// .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_connection_status(
|
||||
// &self,
|
||||
// status: &client::Status,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Option<AnyElement<Self>> {
|
||||
// enum ConnectionStatusButton {}
|
||||
|
||||
// let theme = &theme::current(cx).clone();
|
||||
// match status {
|
||||
// client::Status::ConnectionError
|
||||
// | client::Status::ConnectionLost
|
||||
// | client::Status::Reauthenticating { .. }
|
||||
// | client::Status::Reconnecting { .. }
|
||||
// | client::Status::ReconnectionError { .. } => Some(
|
||||
// Svg::new("icons/disconnected.svg")
|
||||
// .with_color(theme.titlebar.offline_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.titlebar.offline_icon.width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.titlebar.offline_icon.container)
|
||||
// .into_any(),
|
||||
// ),
|
||||
// client::Status::UpgradeRequired => {
|
||||
// let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
// let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
// Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
// Some(AutoUpdateStatus::Installing)
|
||||
// | Some(AutoUpdateStatus::Downloading)
|
||||
// | Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
// Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
// "Please update Zed to Collaborate"
|
||||
// }
|
||||
// };
|
||||
|
||||
// Some(
|
||||
// MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
// Label::new(label, theme.titlebar.outdated_warning.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.titlebar.outdated_warning.container)
|
||||
// .aligned()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, _, cx| {
|
||||
// if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
// if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
// workspace::restart(&Default::default(), cx);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// auto_update::check(&Default::default(), cx);
|
||||
// })
|
||||
// .into_any(),
|
||||
// )
|
||||
// }
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
} else {
|
||||
popover_menu("user-menu")
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
})
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_0p5()
|
||||
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
|
||||
// vcs_menu::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
|
|
|
@ -17,7 +17,7 @@ impl RenderOnce for FacePile {
|
|||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div()
|
||||
.z_index((player_count - ix) as u32)
|
||||
.z_index((player_count - ix) as u8)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
|
|
|
@ -1250,6 +1250,7 @@ impl CompletionsMenu {
|
|||
let documentation_label =
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
Some(SharedString::from(text.clone()))
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -9296,7 +9297,7 @@ impl Render for Editor {
|
|||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = match self.mode {
|
||||
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
|
@ -9309,7 +9310,7 @@ impl Render for Editor {
|
|||
},
|
||||
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features,
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
|
@ -9762,19 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||
.px_1p5()
|
||||
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.invisible()
|
||||
.group_hover(group_id, |style| style.visible())
|
||||
.child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id)
|
||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
)
|
||||
}))
|
||||
.into_any_element()
|
||||
|
|
|
@ -23,13 +23,14 @@ use anyhow::Result;
|
|||
use collections::{BTreeMap, HashMap};
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{
|
||||
div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement,
|
||||
AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla,
|
||||
InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
|
||||
Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
|
||||
div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
|
||||
AnchorCorner, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementId,
|
||||
ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
|
||||
IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
|
||||
ViewContext, WeakView, WindowContext, WrappedLine,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{language_settings::ShowWhitespaceSetting, Language};
|
||||
|
@ -620,20 +621,8 @@ impl EditorElement {
|
|||
let scroll_top =
|
||||
layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
|
||||
let gutter_bg = cx.theme().colors().editor_gutter_background;
|
||||
cx.paint_quad(
|
||||
gutter_bounds,
|
||||
Corners::default(),
|
||||
gutter_bg,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
cx.paint_quad(
|
||||
text_bounds,
|
||||
Corners::default(),
|
||||
self.style.background,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
cx.paint_quad(fill(gutter_bounds, gutter_bg));
|
||||
cx.paint_quad(fill(text_bounds, self.style.background));
|
||||
|
||||
if let EditorMode::Full = layout.mode {
|
||||
let mut active_rows = layout.active_rows.iter().peekable();
|
||||
|
@ -657,13 +646,7 @@ impl EditorElement {
|
|||
layout.position_map.line_height * (end_row - start_row + 1) as f32,
|
||||
);
|
||||
let active_line_bg = cx.theme().colors().editor_active_line_background;
|
||||
cx.paint_quad(
|
||||
Bounds { origin, size },
|
||||
Corners::default(),
|
||||
active_line_bg,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -679,13 +662,7 @@ impl EditorElement {
|
|||
layout.position_map.line_height * highlighted_rows.len() as f32,
|
||||
);
|
||||
let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
|
||||
cx.paint_quad(
|
||||
Bounds { origin, size },
|
||||
Corners::default(),
|
||||
highlighted_line_bg,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
|
||||
}
|
||||
|
||||
let scroll_left =
|
||||
|
@ -706,16 +683,13 @@ impl EditorElement {
|
|||
} else {
|
||||
cx.theme().colors().editor_wrap_guide
|
||||
};
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: point(x, text_bounds.origin.y),
|
||||
size: size(px(1.), text_bounds.size.height),
|
||||
},
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -812,13 +786,13 @@ impl EditorElement {
|
|||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
gpui::yellow(), // todo!("use the right color")
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -845,13 +819,13 @@ impl EditorElement {
|
|||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(1. * line_height),
|
||||
cx.theme().status().deleted,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -867,13 +841,13 @@ impl EditorElement {
|
|||
let highlight_origin = bounds.origin + point(-width, start_y);
|
||||
let highlight_size = size(width * 2., end_y - start_y);
|
||||
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
highlight_bounds,
|
||||
Corners::all(0.05 * line_height),
|
||||
color, // todo!("use the right color")
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -959,7 +933,7 @@ impl EditorElement {
|
|||
cx.stop_propagation();
|
||||
},
|
||||
))
|
||||
.draw(
|
||||
.draw_and_update_state(
|
||||
fold_bounds.origin,
|
||||
fold_bounds.size,
|
||||
cx,
|
||||
|
@ -1120,123 +1094,119 @@ impl EditorElement {
|
|||
cursor.paint(content_origin, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.with_z_index(1, |cx| {
|
||||
if let Some((position, mut context_menu)) = layout.context_menu.take() {
|
||||
let available_space =
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
let context_menu_size = context_menu.measure(available_space, cx);
|
||||
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(position.row() - start_row) as usize]
|
||||
.line;
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize)
|
||||
- layout.position_map.scroll_position.x;
|
||||
let y = (position.row() + 1) as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
let mut list_origin = content_origin + point(x, y);
|
||||
let list_width = context_menu_size.width;
|
||||
let list_height = context_menu_size.height;
|
||||
|
||||
// Snap the right edge of the list to the right edge of the window if
|
||||
// its horizontal bounds overflow.
|
||||
if list_origin.x + list_width > cx.viewport_size().width {
|
||||
list_origin.x =
|
||||
(cx.viewport_size().width - list_width).max(Pixels::ZERO);
|
||||
}
|
||||
|
||||
if list_origin.y + list_height > text_bounds.lower_right().y {
|
||||
list_origin.y -= layout.position_map.line_height + list_height;
|
||||
}
|
||||
|
||||
cx.break_content_mask(|cx| {
|
||||
context_menu.draw(list_origin, available_space, cx)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
|
||||
let available_space =
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout = &layout.position_map.line_layouts
|
||||
[(position.row() - start_row) as usize]
|
||||
.line;
|
||||
|
||||
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
|
||||
// height. This is the size we will use to decide whether to render popovers above or below
|
||||
// the hovered line.
|
||||
let first_size = hover_popovers[0].measure(available_space, cx);
|
||||
let height_to_reserve = first_size.height
|
||||
+ 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
|
||||
|
||||
// Compute Hovered Point
|
||||
let x = hovered_row_layout.x_for_index(position.column() as usize)
|
||||
- layout.position_map.scroll_position.x;
|
||||
let y = position.row() as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
let hovered_point = content_origin + point(x, y);
|
||||
|
||||
if hovered_point.y - height_to_reserve > Pixels::ZERO {
|
||||
// There is enough space above. Render popovers above the hovered point
|
||||
let mut current_y = hovered_point.y;
|
||||
for mut hover_popover in hover_popovers {
|
||||
let size = hover_popover.measure(available_space, cx);
|
||||
let mut popover_origin =
|
||||
point(hovered_point.x, current_y - size.height);
|
||||
|
||||
let x_out_of_bounds =
|
||||
text_bounds.upper_right().x - (popover_origin.x + size.width);
|
||||
if x_out_of_bounds < Pixels::ZERO {
|
||||
popover_origin.x = popover_origin.x + x_out_of_bounds;
|
||||
}
|
||||
|
||||
cx.break_content_mask(|cx| {
|
||||
hover_popover.draw(popover_origin, available_space, cx)
|
||||
});
|
||||
|
||||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||||
}
|
||||
} else {
|
||||
// There is not enough space above. Render popovers below the hovered point
|
||||
let mut current_y = hovered_point.y + layout.position_map.line_height;
|
||||
for mut hover_popover in hover_popovers {
|
||||
let size = hover_popover.measure(available_space, cx);
|
||||
let mut popover_origin = point(hovered_point.x, current_y);
|
||||
|
||||
let x_out_of_bounds =
|
||||
text_bounds.upper_right().x - (popover_origin.x + size.width);
|
||||
if x_out_of_bounds < Pixels::ZERO {
|
||||
popover_origin.x = popover_origin.x + x_out_of_bounds;
|
||||
}
|
||||
|
||||
hover_popover.draw(popover_origin, available_space, cx);
|
||||
|
||||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mouse_context_menu) =
|
||||
self.editor.read(cx).mouse_context_menu.as_ref()
|
||||
{
|
||||
let element = overlay()
|
||||
.position(mouse_context_menu.position)
|
||||
.child(mouse_context_menu.context_menu.clone())
|
||||
.anchor(AnchorCorner::TopLeft)
|
||||
.snap_to_window();
|
||||
element.draw(
|
||||
gpui::Point::default(),
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent),
|
||||
cx,
|
||||
|_, _| {},
|
||||
);
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint_overlays(
|
||||
&mut self,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
if let Some((position, mut context_menu)) = layout.context_menu.take() {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
let context_menu_size = context_menu.measure(available_space, cx);
|
||||
|
||||
let cursor_row_layout =
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize)
|
||||
- layout.position_map.scroll_position.x;
|
||||
let y = (position.row() + 1) as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
let mut list_origin = content_origin + point(x, y);
|
||||
let list_width = context_menu_size.width;
|
||||
let list_height = context_menu_size.height;
|
||||
|
||||
// Snap the right edge of the list to the right edge of the window if
|
||||
// its horizontal bounds overflow.
|
||||
if list_origin.x + list_width > cx.viewport_size().width {
|
||||
list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
|
||||
}
|
||||
|
||||
if list_origin.y + list_height > text_bounds.lower_right().y {
|
||||
list_origin.y -= layout.position_map.line_height + list_height;
|
||||
}
|
||||
|
||||
cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx));
|
||||
}
|
||||
|
||||
if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout =
|
||||
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
|
||||
|
||||
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
|
||||
// height. This is the size we will use to decide whether to render popovers above or below
|
||||
// the hovered line.
|
||||
let first_size = hover_popovers[0].measure(available_space, cx);
|
||||
let height_to_reserve =
|
||||
first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
|
||||
|
||||
// Compute Hovered Point
|
||||
let x = hovered_row_layout.x_for_index(position.column() as usize)
|
||||
- layout.position_map.scroll_position.x;
|
||||
let y = position.row() as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
let hovered_point = content_origin + point(x, y);
|
||||
|
||||
if hovered_point.y - height_to_reserve > Pixels::ZERO {
|
||||
// There is enough space above. Render popovers above the hovered point
|
||||
let mut current_y = hovered_point.y;
|
||||
for mut hover_popover in hover_popovers {
|
||||
let size = hover_popover.measure(available_space, cx);
|
||||
let mut popover_origin = point(hovered_point.x, current_y - size.height);
|
||||
|
||||
let x_out_of_bounds =
|
||||
text_bounds.upper_right().x - (popover_origin.x + size.width);
|
||||
if x_out_of_bounds < Pixels::ZERO {
|
||||
popover_origin.x = popover_origin.x + x_out_of_bounds;
|
||||
}
|
||||
|
||||
cx.break_content_mask(|cx| {
|
||||
hover_popover.draw(popover_origin, available_space, cx)
|
||||
});
|
||||
|
||||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||||
}
|
||||
} else {
|
||||
// There is not enough space above. Render popovers below the hovered point
|
||||
let mut current_y = hovered_point.y + layout.position_map.line_height;
|
||||
for mut hover_popover in hover_popovers {
|
||||
let size = hover_popover.measure(available_space, cx);
|
||||
let mut popover_origin = point(hovered_point.x, current_y);
|
||||
|
||||
let x_out_of_bounds =
|
||||
text_bounds.upper_right().x - (popover_origin.x + size.width);
|
||||
if x_out_of_bounds < Pixels::ZERO {
|
||||
popover_origin.x = popover_origin.x + x_out_of_bounds;
|
||||
}
|
||||
|
||||
hover_popover.draw(popover_origin, available_space, cx);
|
||||
|
||||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() {
|
||||
let element = overlay()
|
||||
.position(mouse_context_menu.position)
|
||||
.child(mouse_context_menu.context_menu.clone())
|
||||
.anchor(AnchorCorner::TopLeft)
|
||||
.snap_to_window();
|
||||
element.into_any().draw(
|
||||
gpui::Point::default(),
|
||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
|
||||
bounds.upper_right().x - self.style.scrollbar_width
|
||||
}
|
||||
|
@ -1278,7 +1248,7 @@ impl EditorElement {
|
|||
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
|
||||
|
||||
if layout.show_scrollbars {
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
track_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_track_background,
|
||||
|
@ -1289,7 +1259,7 @@ impl EditorElement {
|
|||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
);
|
||||
));
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
if layout.is_singleton && scrollbar_settings.selections {
|
||||
let start_anchor = Anchor::min();
|
||||
|
@ -1309,7 +1279,7 @@ impl EditorElement {
|
|||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
|
@ -1320,7 +1290,7 @@ impl EditorElement {
|
|||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1352,7 +1322,7 @@ impl EditorElement {
|
|||
DiffHunkStatus::Modified => cx.theme().status().modified,
|
||||
DiffHunkStatus::Removed => cx.theme().status().deleted,
|
||||
};
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
|
@ -1363,11 +1333,11 @@ impl EditorElement {
|
|||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
|
@ -1378,7 +1348,7 @@ impl EditorElement {
|
|||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
let mouse_position = cx.mouse_position();
|
||||
|
@ -1525,7 +1495,7 @@ impl EditorElement {
|
|||
let scroll_left = scroll_position.x * layout.position_map.em_width;
|
||||
let scroll_top = scroll_position.y * layout.position_map.line_height;
|
||||
|
||||
for block in layout.blocks.drain(..) {
|
||||
for mut block in layout.blocks.drain(..) {
|
||||
let mut origin = bounds.origin
|
||||
+ point(
|
||||
Pixels::ZERO,
|
||||
|
@ -2810,7 +2780,7 @@ impl Element for EditorElement {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
mut self,
|
||||
&mut self,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut gpui::WindowContext,
|
||||
|
@ -2833,32 +2803,30 @@ impl Element for EditorElement {
|
|||
self.register_actions(cx);
|
||||
self.register_key_listeners(cx);
|
||||
|
||||
// We call with_z_index to establish a new stacking context.
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
// Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
|
||||
// take precedence.
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
|
||||
});
|
||||
let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
|
||||
cx.handle_input(&focus_handle, input_handler);
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
|
||||
cx.handle_input(&focus_handle, input_handler);
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
|
||||
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
});
|
||||
})
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
|
||||
cx.with_z_index(1, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
|
||||
|
||||
cx.with_z_index(2, |cx| {
|
||||
self.paint_overlays(text_bounds, &mut layout, cx);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
@ -3085,23 +3053,13 @@ impl Cursor {
|
|||
};
|
||||
|
||||
//Draw background or border quad
|
||||
if matches!(self.shape, CursorShape::Hollow) {
|
||||
cx.paint_quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
transparent_black(),
|
||||
Edges::all(px(1.)),
|
||||
self.color,
|
||||
);
|
||||
let cursor = if matches!(self.shape, CursorShape::Hollow) {
|
||||
outline(bounds, self.color)
|
||||
} else {
|
||||
cx.paint_quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
self.color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
}
|
||||
fill(bounds, self.color)
|
||||
};
|
||||
|
||||
cx.paint_quad(cursor);
|
||||
|
||||
if let Some(block_text) = &self.block_text {
|
||||
block_text.paint(self.origin + origin, self.line_height, cx);
|
||||
|
|
|
@ -18,7 +18,6 @@ gpui = { package = "gpui2", path = "../gpui2" }
|
|||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
regex.workspace = true
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
|
@ -26,16 +25,20 @@ ui = { package = "ui2", path = "../ui2" }
|
|||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2"}
|
||||
|
||||
log.workspace = true
|
||||
futures.workspace = true
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
bitflags = "2.4.1"
|
||||
human_bytes = "0.4.1"
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use std::{ops::RangeInclusive, sync::Arc};
|
||||
use std::{ops::RangeInclusive, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use bitflags::bitflags;
|
||||
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Model, PromptLevel, Render, Task, View, ViewContext,
|
||||
div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
|
||||
FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
|
||||
};
|
||||
use isahc::Request;
|
||||
use language::Buffer;
|
||||
|
@ -22,6 +23,7 @@ use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
|
|||
|
||||
// For UI testing purposes
|
||||
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
|
||||
const SEND_TIME_IN_DEV_MODE: Duration = Duration::from_secs(2);
|
||||
|
||||
// Temporary, until tests are in place
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -47,13 +49,32 @@ struct FeedbackRequestBody<'a> {
|
|||
token: &'a str,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct InvalidStateFlags: u8 {
|
||||
const EmailAddress = 0b00000001;
|
||||
const CharacterCount = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CannotSubmitReason {
|
||||
InvalidState { flags: InvalidStateFlags },
|
||||
AwaitingSubmission,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SubmissionState {
|
||||
CanSubmit,
|
||||
CannotSubmit { reason: CannotSubmitReason },
|
||||
}
|
||||
|
||||
pub struct FeedbackModal {
|
||||
system_specs: SystemSpecs,
|
||||
feedback_editor: View<Editor>,
|
||||
email_address_editor: View<Editor>,
|
||||
awaiting_submission: bool,
|
||||
user_submitted: bool,
|
||||
discarded: bool,
|
||||
submission_state: Option<SubmissionState>,
|
||||
dismiss_modal: bool,
|
||||
character_count: i32,
|
||||
}
|
||||
|
||||
|
@ -66,12 +87,7 @@ impl EventEmitter<DismissEvent> for FeedbackModal {}
|
|||
|
||||
impl ModalView for FeedbackModal {
|
||||
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.user_submitted {
|
||||
self.set_user_submitted(false, cx);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.discarded {
|
||||
if self.dismiss_modal {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -85,7 +101,7 @@ impl ModalView for FeedbackModal {
|
|||
cx.spawn(move |this, mut cx| async move {
|
||||
if answer.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.discarded = true;
|
||||
this.dismiss_modal = true;
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.log_err();
|
||||
|
@ -159,32 +175,27 @@ impl FeedbackModal {
|
|||
editor
|
||||
});
|
||||
|
||||
cx.subscribe(
|
||||
&feedback_editor,
|
||||
|this, editor, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::Edited => {
|
||||
this.character_count = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("Feedback editor is never a multi-buffer")
|
||||
.read(cx)
|
||||
.len() as i32;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
|
||||
if *event == EditorEvent::Edited {
|
||||
this.character_count = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("Feedback editor is never a multi-buffer")
|
||||
.read(cx)
|
||||
.len() as i32;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
system_specs: system_specs.clone(),
|
||||
feedback_editor,
|
||||
email_address_editor,
|
||||
awaiting_submission: false,
|
||||
user_submitted: false,
|
||||
discarded: false,
|
||||
submission_state: None,
|
||||
dismiss_modal: false,
|
||||
character_count: 0,
|
||||
}
|
||||
}
|
||||
|
@ -205,19 +216,24 @@ impl FeedbackModal {
|
|||
if answer == Some(0) {
|
||||
match email.clone() {
|
||||
Some(email) => {
|
||||
let _ = KEY_VALUE_STORE
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(DATABASE_KEY_NAME.to_string(), email)
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
None => {
|
||||
let _ = KEY_VALUE_STORE
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(DATABASE_KEY_NAME.to_string())
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_awaiting_submission(true, cx);
|
||||
this.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
|
@ -227,7 +243,8 @@ impl FeedbackModal {
|
|||
match res {
|
||||
Ok(_) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_user_submitted(true, cx);
|
||||
this.dismiss_modal = true;
|
||||
cx.notify();
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.ok();
|
||||
|
@ -244,7 +261,9 @@ impl FeedbackModal {
|
|||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
this.set_awaiting_submission(false, cx);
|
||||
|
||||
this.submission_state = Some(SubmissionState::CanSubmit);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
@ -256,16 +275,6 @@ impl FeedbackModal {
|
|||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_awaiting_submission(&mut self, awaiting_submission: bool, cx: &mut ViewContext<Self>) {
|
||||
self.awaiting_submission = awaiting_submission;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext<Self>) {
|
||||
self.user_submitted = user_submitted;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
async fn submit_feedback(
|
||||
feedback_text: &str,
|
||||
email: Option<String>,
|
||||
|
@ -273,6 +282,8 @@ impl FeedbackModal {
|
|||
system_specs: SystemSpecs,
|
||||
) -> anyhow::Result<()> {
|
||||
if DEV_MODE {
|
||||
smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
|
||||
|
||||
if SEND_SUCCESS_IN_DEV_MODE {
|
||||
return Ok(());
|
||||
} else {
|
||||
|
@ -309,7 +320,67 @@ impl FeedbackModal {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Escape button calls dismiss
|
||||
fn update_submission_state(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.awaiting_submission() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut invalid_state_flags = InvalidStateFlags::empty();
|
||||
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
|
||||
if !valid_email_address {
|
||||
invalid_state_flags |= InvalidStateFlags::EmailAddress;
|
||||
}
|
||||
|
||||
if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
|
||||
invalid_state_flags |= InvalidStateFlags::CharacterCount;
|
||||
}
|
||||
|
||||
if invalid_state_flags.is_empty() {
|
||||
self.submission_state = Some(SubmissionState::CanSubmit);
|
||||
} else {
|
||||
self.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState {
|
||||
flags: invalid_state_flags,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_email_address(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::EmailAddress)
|
||||
}
|
||||
|
||||
fn valid_character_count(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::CharacterCount)
|
||||
}
|
||||
|
||||
fn in_invalid_state(&self, flag: InvalidStateFlags) -> bool {
|
||||
match self.submission_state {
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState { ref flags },
|
||||
}) => flags.contains(flag),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn awaiting_submission(&self) -> bool {
|
||||
matches!(
|
||||
self.submission_state,
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.submission_state, Some(SubmissionState::CanSubmit))
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
@ -319,17 +390,9 @@ impl Render for FeedbackModal {
|
|||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
self.update_submission_state(cx);
|
||||
|
||||
let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count);
|
||||
|
||||
let allow_submission =
|
||||
valid_character_count && valid_email_address && !self.awaiting_submission;
|
||||
|
||||
let submit_button_text = if self.awaiting_submission {
|
||||
let submit_button_text = if self.awaiting_submission() {
|
||||
"Submitting..."
|
||||
} else {
|
||||
"Submit"
|
||||
|
@ -367,7 +430,7 @@ impl Render for FeedbackModal {
|
|||
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
|
||||
)
|
||||
})
|
||||
.color(if valid_character_count {
|
||||
.color(if self.valid_character_count() {
|
||||
Color::Success
|
||||
} else {
|
||||
Color::Error
|
||||
|
@ -391,7 +454,11 @@ impl Render for FeedbackModal {
|
|||
.p_2()
|
||||
.border()
|
||||
.rounded_md()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
red()
|
||||
})
|
||||
.child(self.email_address_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
|
@ -424,11 +491,9 @@ impl Render for FeedbackModal {
|
|||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("send_feedback", submit_button_text)
|
||||
Button::new("submit_feedback", submit_button_text)
|
||||
.color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
// TODO: Ensure that while submitting, "Sending..." is shown and disable the button
|
||||
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.submit(cx).detach();
|
||||
}))
|
||||
|
@ -440,7 +505,7 @@ impl Render for FeedbackModal {
|
|||
cx,
|
||||
)
|
||||
})
|
||||
.when(!allow_submission, |this| this.disabled(true)),
|
||||
.when(!self.can_submit(), |this| this.disabled(true)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -450,3 +515,42 @@ impl Render for FeedbackModal {
|
|||
|
||||
// TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
|
||||
// TODO: Testing of various button states, dismissal prompts, etc.
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn test_invalid_email_addresses() {
|
||||
// let markdown = markdown.await.log_err();
|
||||
// let buffer = project.update(&mut cx, |project, cx| {
|
||||
// project.create_buffer("", markdown, cx)
|
||||
// })??;
|
||||
|
||||
// workspace.update(&mut cx, |workspace, cx| {
|
||||
// let system_specs = SystemSpecs::new(cx);
|
||||
|
||||
// workspace.toggle_modal(cx, move |cx| {
|
||||
// let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx);
|
||||
|
||||
// assert!(!feedback_modal.can_submit());
|
||||
// assert!(!feedback_modal.valid_email_address(cx));
|
||||
// assert!(!feedback_modal.valid_character_count());
|
||||
|
||||
// feedback_modal
|
||||
// .email_address_editor
|
||||
// .update(cx, |this, cx| this.set_text("a", cx));
|
||||
// feedback_modal.set_submission_state(cx);
|
||||
|
||||
// assert!(!feedback_modal.valid_email_address(cx));
|
||||
|
||||
// feedback_modal
|
||||
// .email_address_editor
|
||||
// .update(cx, |this, cx| this.set_text("a&b.com", cx));
|
||||
// feedback_modal.set_submission_state(cx);
|
||||
|
||||
// assert!(feedback_modal.valid_email_address(cx));
|
||||
// });
|
||||
// })?;
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -1091,6 +1091,12 @@ impl AppContext {
|
|||
pub fn has_active_drag(&self) -> bool {
|
||||
self.active_drag.is_some()
|
||||
}
|
||||
|
||||
pub fn active_drag<T: 'static>(&self) -> Option<&T> {
|
||||
self.active_drag
|
||||
.as_ref()
|
||||
.and_then(|drag| drag.value.downcast_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
@ -1241,6 +1247,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
|
|||
/// within the window or by dragging into the app from the underlying platform.
|
||||
pub struct AnyDrag {
|
||||
pub view: AnyView,
|
||||
pub value: Box<dyn Any>,
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ pub trait IntoElement: Sized {
|
|||
self.into_element().into_any()
|
||||
}
|
||||
|
||||
fn draw<T, R>(
|
||||
fn draw_and_update_state<T, R>(
|
||||
self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<T>,
|
||||
|
@ -92,7 +92,7 @@ pub trait Element: 'static + IntoElement {
|
|||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State);
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
|
||||
|
||||
fn into_any(self) -> AnyElement {
|
||||
AnyElement::new(self)
|
||||
|
@ -150,8 +150,8 @@ impl<C: RenderOnce> Element for Component<C> {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
let element = state.rendered_element.take().unwrap();
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
let mut element = state.rendered_element.take().unwrap();
|
||||
if let Some(element_id) = element.element_id() {
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
|
@ -420,7 +420,7 @@ impl AnyElement {
|
|||
self.0.layout(cx)
|
||||
}
|
||||
|
||||
pub fn paint(mut self, cx: &mut WindowContext) {
|
||||
pub fn paint(&mut self, cx: &mut WindowContext) {
|
||||
self.0.paint(cx)
|
||||
}
|
||||
|
||||
|
@ -435,7 +435,7 @@ impl AnyElement {
|
|||
|
||||
/// Initializes this element and performs layout in the available space, then paints it at the given origin.
|
||||
pub fn draw(
|
||||
mut self,
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
available_space: Size<AvailableSpace>,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -465,8 +465,8 @@ impl Element for AnyElement {
|
|||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.paint(cx);
|
||||
fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
|
||||
self.paint(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,48 +482,37 @@ impl IntoElement for AnyElement {
|
|||
}
|
||||
}
|
||||
|
||||
// impl<V, E, F> Element for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
|
||||
// {
|
||||
// type State = Option<AnyElement>;
|
||||
/// The empty element, which renders nothing.
|
||||
pub type Empty = ();
|
||||
|
||||
// fn element_id(&self) -> Option<ElementId> {
|
||||
// None
|
||||
// }
|
||||
impl IntoElement for () {
|
||||
type Element = Self;
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// _: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let render = self.take().unwrap();
|
||||
// let mut element = (render)(view_state, cx).into_any();
|
||||
// let layout_id = element.layout(view_state, cx);
|
||||
// (layout_id, Some(element))
|
||||
// }
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
// fn paint(
|
||||
// self,
|
||||
// _bounds: Bounds<Pixels>,
|
||||
// rendered_element: &mut Self::State,
|
||||
// cx: &mut WindowContext,
|
||||
// ) {
|
||||
// rendered_element.take().unwrap().paint(view_state, cx);
|
||||
// }
|
||||
// }
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// impl<V, E, F> RenderOnce for Option<F>
|
||||
// where
|
||||
// V: 'static,
|
||||
// E: Element,
|
||||
// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
|
||||
// {
|
||||
// type Element = Self;
|
||||
impl Element for () {
|
||||
type State = ();
|
||||
|
||||
// fn render(self) -> Self::Element {
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
fn layout(
|
||||
&mut self,
|
||||
_state: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
(cx.request_layout(&crate::Style::default(), None), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::State,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,15 @@ use refineable::Refineable as _;
|
|||
|
||||
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
|
||||
|
||||
pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
|
||||
Canvas {
|
||||
paint_callback: Box::new(callback),
|
||||
paint_callback: Some(Box::new(callback)),
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Canvas {
|
||||
paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
|
||||
paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ impl IntoElement for Canvas {
|
|||
}
|
||||
|
||||
impl Element for Canvas {
|
||||
type State = ();
|
||||
type State = Style;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
|
@ -37,11 +37,13 @@ impl Element for Canvas {
|
|||
let mut style = Style::default();
|
||||
style.refine(&self.style);
|
||||
let layout_id = cx.request_layout(&style, []);
|
||||
(layout_id, ())
|
||||
(layout_id, style)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
|
||||
(self.paint_callback)(bounds, cx)
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
|
||||
style.paint(bounds, cx, |cx| {
|
||||
(self.paint_callback.take().unwrap())(&bounds, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -81,11 +81,12 @@ impl Element for Img {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let source = self.source.clone();
|
||||
self.interactivity.paint(
|
||||
bounds,
|
||||
bounds.size,
|
||||
|
@ -94,7 +95,7 @@ impl Element for Img {
|
|||
|style, _scroll_offset, cx| {
|
||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
match self.source {
|
||||
match source {
|
||||
ImageSource::Uri(uri) => {
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
if let Some(data) = image_future
|
||||
|
|
|
@ -257,7 +257,7 @@ impl Element for List {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
_state: &mut Self::State,
|
||||
cx: &mut crate::WindowContext,
|
||||
|
@ -385,7 +385,7 @@ impl Element for List {
|
|||
// Paint the visible items
|
||||
let mut item_origin = bounds.origin;
|
||||
item_origin.y -= scroll_top.offset_in_item;
|
||||
for mut item_element in item_elements {
|
||||
for item_element in &mut item_elements {
|
||||
let item_height = item_element.measure(available_item_space, cx).height;
|
||||
item_element.draw(item_origin, available_item_space, cx);
|
||||
item_origin.y += item_height;
|
||||
|
|
|
@ -81,7 +81,7 @@ impl Element for Overlay {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -149,7 +149,7 @@ impl Element for Overlay {
|
|||
|
||||
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||
cx.break_content_mask(|cx| {
|
||||
for child in self.children {
|
||||
for child in &mut self.children {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -36,8 +36,12 @@ impl Element for Svg {
|
|||
})
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
|
||||
where
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
|
||||
use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
impl Element for &'static str {
|
||||
|
@ -22,7 +22,7 @@ impl Element for &'static str {
|
|||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
state.paint(bounds, self, cx)
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ impl Element for SharedString {
|
|||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
|
||||
let text_str: &str = self.as_ref();
|
||||
state.paint(bounds, text_str, cx)
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ impl Element for StyledText {
|
|||
(layout_id, state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state.paint(bounds, &self.text, cx)
|
||||
}
|
||||
}
|
||||
|
@ -356,8 +356,8 @@ impl Element for InteractiveText {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
if let Some(click_listener) = self.click_listener {
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
if let Some(ix) = state
|
||||
.text_state
|
||||
.index_for_position(bounds, cx.mouse_position())
|
||||
|
@ -374,13 +374,14 @@ impl Element for InteractiveText {
|
|||
let text_state = state.text_state.clone();
|
||||
let mouse_down = state.mouse_down_index.clone();
|
||||
if let Some(mouse_down_index) = mouse_down.get() {
|
||||
let clickable_ranges = mem::take(&mut self.clickable_ranges);
|
||||
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
if let Some(mouse_up_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
{
|
||||
click_listener(
|
||||
&self.clickable_ranges,
|
||||
&clickable_ranges,
|
||||
InteractiveTextClickEvent {
|
||||
mouse_down_index,
|
||||
mouse_up_index,
|
||||
|
|
|
@ -10,6 +10,7 @@ use taffy::style::Overflow;
|
|||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
#[track_caller]
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: View<V>,
|
||||
id: I,
|
||||
|
@ -42,6 +43,10 @@ where
|
|||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
base_style: Box::new(base_style),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
location: Some(*core::panic::Location::caller()),
|
||||
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
|
@ -150,7 +155,7 @@ impl Element for UniformList {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
|
@ -197,41 +202,41 @@ impl Element for UniformList {
|
|||
);
|
||||
|
||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
|
||||
style.paint(bounds, cx);
|
||||
style.paint(bounds, cx, |cx| {
|
||||
if self.item_count > 0 {
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
list_height: padded_bounds.size.height,
|
||||
scroll_offset: shared_scroll_offset,
|
||||
});
|
||||
}
|
||||
|
||||
if self.item_count > 0 {
|
||||
if let Some(scroll_handle) = self.scroll_handle.clone() {
|
||||
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
|
||||
item_height,
|
||||
list_height: padded_bounds.size.height,
|
||||
scroll_offset: shared_scroll_offset,
|
||||
let first_visible_element_ix =
|
||||
(-scroll_offset.y / item_height).floor() as usize;
|
||||
let last_visible_element_ix =
|
||||
((-scroll_offset.y + padded_bounds.size.height) / item_height)
|
||||
.ceil() as usize;
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let mut items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in items.iter_mut().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.draw(item_origin, available_space, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let first_visible_element_ix =
|
||||
(-scroll_offset.y / item_height).floor() as usize;
|
||||
let last_visible_element_ix =
|
||||
((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
|
||||
as usize;
|
||||
let visible_range = first_visible_element_ix
|
||||
..cmp::min(last_visible_element_ix, self.item_count);
|
||||
|
||||
let items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.draw(item_origin, available_space, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1592,6 +1592,17 @@ impl Edges<Pixels> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Into<Edges<Pixels>> for f32 {
|
||||
fn into(self) -> Edges<Pixels> {
|
||||
Edges {
|
||||
top: self.into(),
|
||||
right: self.into(),
|
||||
bottom: self.into(),
|
||||
left: self.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the corners of a box in a 2D space, such as border radius.
|
||||
///
|
||||
/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
|
||||
|
@ -1808,6 +1819,28 @@ where
|
|||
|
||||
impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
|
||||
|
||||
impl Into<Corners<Pixels>> for f32 {
|
||||
fn into(self) -> Corners<Pixels> {
|
||||
Corners {
|
||||
top_left: self.into(),
|
||||
top_right: self.into(),
|
||||
bottom_right: self.into(),
|
||||
bottom_left: self.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Corners<Pixels>> for Pixels {
|
||||
fn into(self) -> Corners<Pixels> {
|
||||
Corners {
|
||||
top_left: self,
|
||||
top_right: self,
|
||||
bottom_right: self,
|
||||
bottom_left: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a length in pixels, the base unit of measurement in the UI framework.
|
||||
///
|
||||
/// `Pixels` is a value type that represents an absolute length in pixels, which is used
|
||||
|
|
|
@ -147,6 +147,7 @@ pub trait PlatformWindow {
|
|||
fn appearance(&self) -> WindowAppearance;
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||
fn mouse_position(&self) -> Point<Pixels>;
|
||||
fn modifiers(&self) -> Modifiers;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
|
||||
fn clear_input_handler(&mut self);
|
||||
|
|
|
@ -187,6 +187,8 @@ impl MetalRenderer {
|
|||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let layer = self.layer.clone();
|
||||
let viewport_size = layer.drawable_size();
|
||||
let viewport_size: Size<DevicePixels> = size(
|
||||
|
@ -303,6 +305,10 @@ impl MetalRenderer {
|
|||
|
||||
command_buffer.commit();
|
||||
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
let duration_since_start = start.elapsed();
|
||||
println!("renderer draw: {:?}", duration_since_start);
|
||||
|
||||
command_buffer.wait_until_completed();
|
||||
drawable.present();
|
||||
}
|
||||
|
|
|
@ -9,9 +9,10 @@ use crate::{
|
|||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard,
|
||||
NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
|
||||
NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
|
||||
NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
|
||||
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
|
||||
NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{
|
||||
|
@ -744,6 +745,26 @@ impl PlatformWindow for MacWindow {
|
|||
convert_mouse_position(position, self.content_size().height)
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
unsafe {
|
||||
let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
|
||||
|
||||
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
|
||||
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
|
||||
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
|
||||
|
||||
Modifiers {
|
||||
control,
|
||||
alt,
|
||||
shift,
|
||||
command,
|
||||
function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
|
|
@ -79,6 +79,10 @@ impl PlatformWindow for TestWindow {
|
|||
Point::default()
|
||||
}
|
||||
|
||||
fn modifiers(&self) -> crate::Modifiers {
|
||||
crate::Modifiers::default()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ pub type LayerId = u32;
|
|||
pub type DrawOrder = u32;
|
||||
|
||||
pub(crate) struct SceneBuilder {
|
||||
last_order: Option<(StackingOrder, LayerId)>,
|
||||
layers_by_order: BTreeMap<StackingOrder, LayerId>,
|
||||
splitter: BspSplitter<(PrimitiveKind, usize)>,
|
||||
shadows: Vec<Shadow>,
|
||||
|
@ -31,6 +32,7 @@ pub(crate) struct SceneBuilder {
|
|||
impl Default for SceneBuilder {
|
||||
fn default() -> Self {
|
||||
SceneBuilder {
|
||||
last_order: None,
|
||||
layers_by_order: BTreeMap::new(),
|
||||
splitter: BspSplitter::new(),
|
||||
shadows: Vec::new(),
|
||||
|
@ -52,6 +54,7 @@ impl SceneBuilder {
|
|||
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
|
||||
}
|
||||
self.layers_by_order.clear();
|
||||
self.last_order = None;
|
||||
|
||||
// Add all primitives to the BSP splitter to determine draw order
|
||||
self.splitter.reset();
|
||||
|
@ -156,14 +159,7 @@ impl SceneBuilder {
|
|||
return;
|
||||
}
|
||||
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
|
||||
let layer_id = self.layer_id_for_order(order);
|
||||
match primitive {
|
||||
Primitive::Shadow(mut shadow) => {
|
||||
shadow.order = layer_id;
|
||||
|
@ -196,6 +192,24 @@ impl SceneBuilder {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 {
|
||||
if let Some((last_order, last_layer_id)) = self.last_order.as_ref() {
|
||||
if last_order == order {
|
||||
return *last_layer_id;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
self.last_order = Some((order.clone(), layer_id));
|
||||
layer_id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::{iter, mem, ops::Range};
|
||||
|
||||
use crate::{
|
||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
|
||||
ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
|
||||
Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
|
||||
};
|
||||
use collections::HashSet;
|
||||
|
@ -14,6 +14,9 @@ pub use taffy::style::{
|
|||
Overflow, Position,
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub struct DebugBelow;
|
||||
|
||||
pub type StyleCascade = Cascade<Style>;
|
||||
|
||||
#[derive(Clone, Refineable, Debug)]
|
||||
|
@ -107,7 +110,12 @@ pub struct Style {
|
|||
/// The mouse cursor style shown when the mouse pointer is over an element.
|
||||
pub mouse_cursor: Option<CursorStyle>,
|
||||
|
||||
pub z_index: Option<u32>,
|
||||
pub z_index: Option<u8>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug: bool,
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_below: bool,
|
||||
}
|
||||
|
||||
impl Styled for StyleRefinement {
|
||||
|
@ -334,7 +342,22 @@ impl Style {
|
|||
}
|
||||
|
||||
/// Paints the background of an element styled with this style.
|
||||
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
|
||||
pub fn paint(
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
continuation: impl FnOnce(&mut WindowContext),
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.set_global(DebugBelow)
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug || cx.has_global::<DebugBelow>() {
|
||||
cx.paint_quad(crate::outline(bounds, crate::red()));
|
||||
}
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
|
@ -348,15 +371,24 @@ impl Style {
|
|||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.is_some() || self.is_border_visible() {
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
background_color.unwrap_or_default(),
|
||||
self.border_widths.to_pixels(rem_size),
|
||||
self.border_color.unwrap_or_default(),
|
||||
);
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
cx.with_z_index(2, |cx| {
|
||||
continuation(cx);
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
cx.remove_global::<DebugBelow>();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_border_visible(&self) -> bool {
|
||||
|
@ -404,6 +436,11 @@ impl Default for Style {
|
|||
text: TextStyleRefinement::default(),
|
||||
mouse_cursor: None,
|
||||
z_index: None,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug: false,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_below: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ pub trait Styled: Sized {
|
|||
|
||||
gpui2_macros::style_helpers!();
|
||||
|
||||
fn z_index(mut self, z_index: u32) -> Self {
|
||||
fn z_index(mut self, z_index: u8) -> Self {
|
||||
self.style().z_index = Some(z_index);
|
||||
self
|
||||
}
|
||||
|
@ -633,4 +633,16 @@ pub trait Styled: Sized {
|
|||
.line_height = Some(line_height.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug(mut self) -> Self {
|
||||
self.style().debug = Some(true);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_below(mut self) -> Self {
|
||||
self.style().debug_below = Some(true);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::{
|
||||
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
|
||||
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
|
||||
WrappedLineLayout,
|
||||
black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
|
||||
SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
|
||||
};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -109,16 +108,13 @@ fn paint_line(
|
|||
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
|
||||
wraps.next();
|
||||
if let Some((background_origin, background_color)) = current_background.as_mut() {
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: *background_origin,
|
||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
*background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
background_origin.x = origin.x;
|
||||
background_origin.y += line_height;
|
||||
}
|
||||
|
@ -180,16 +176,13 @@ fn paint_line(
|
|||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = finished_background {
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
size: size(glyph_origin.x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
if let Some((underline_origin, underline_style)) = finished_underline {
|
||||
|
@ -235,16 +228,13 @@ fn paint_line(
|
|||
}
|
||||
|
||||
if let Some((background_origin, background_color)) = current_background.take() {
|
||||
cx.paint_quad(
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: background_origin,
|
||||
size: size(last_line_end_x - background_origin.x, line_height),
|
||||
},
|
||||
Corners::default(),
|
||||
background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
if let Some((underline_start, underline_style)) = current_underline.take() {
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::{
|
|||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
|
@ -90,7 +91,7 @@ impl<V: Render> Element for View<V> {
|
|||
(layout_id, Some(element))
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
|
||||
fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
|
||||
element.take().unwrap().paint(cx);
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +171,7 @@ impl<V> Eq for WeakView<V> {}
|
|||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyView {
|
||||
|
@ -208,9 +209,20 @@ impl AnyView {
|
|||
cx: &mut WindowContext,
|
||||
) {
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
let (layout_id, rendered_element) = (self.layout)(self, cx);
|
||||
let start_time = std::time::Instant::now();
|
||||
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
|
||||
let duration = start_time.elapsed();
|
||||
println!("request layout: {:?}", duration);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
cx.compute_layout(layout_id, available_space);
|
||||
(self.paint)(self, rendered_element, cx);
|
||||
let duration = start_time.elapsed();
|
||||
println!("compute layout: {:?}", duration);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
(self.paint)(self, &mut rendered_element, cx);
|
||||
let duration = start_time.elapsed();
|
||||
println!("paint: {:?}", duration);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -237,12 +249,12 @@ impl Element for AnyView {
|
|||
(layout_id, Some(state))
|
||||
}
|
||||
|
||||
fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
fn paint(&mut self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
debug_assert!(
|
||||
state.is_some(),
|
||||
"state is None. Did you include an AnyView twice in the tree?"
|
||||
);
|
||||
(self.paint)(&self, state.take().unwrap(), cx)
|
||||
(self.paint)(&self, state.as_mut().unwrap(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,7 +285,7 @@ impl IntoElement for AnyView {
|
|||
pub struct AnyWeakView {
|
||||
model: AnyWeakModel,
|
||||
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
|
||||
paint: fn(&AnyView, AnyElement, &mut WindowContext),
|
||||
paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
|
||||
}
|
||||
|
||||
impl AnyWeakView {
|
||||
|
@ -297,6 +309,20 @@ impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyWeakView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.model == other.model
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnyWeakView {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AnyWeakView")
|
||||
.field("entity_id", &self.model.entity_id)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Render for T
|
||||
where
|
||||
T: 'static + FnMut(&mut WindowContext) -> E,
|
||||
|
@ -324,7 +350,7 @@ mod any_view {
|
|||
|
||||
pub(crate) fn paint<V: 'static + Render>(
|
||||
_view: &AnyView,
|
||||
element: AnyElement,
|
||||
element: &mut AnyElement,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
element.paint(cx);
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use crate::{
|
||||
key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
|
||||
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
|
||||
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
|
||||
EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
|
||||
InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId, Model,
|
||||
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
key_dispatch::DispatchActionListener, px, size, transparent_black, Action, AnyDrag, AnyView,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners,
|
||||
CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
|
||||
EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
|
||||
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
|
||||
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
|
||||
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
|
||||
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
|
||||
RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
|
||||
SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
|
||||
VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use collections::FxHashMap;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
|
@ -38,12 +38,24 @@ use std::{
|
|||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const ACTIVE_DRAG_Z_INDEX: u32 = 1;
|
||||
const ACTIVE_DRAG_Z_INDEX: u8 = 1;
|
||||
|
||||
/// A global stacking order, which is created by stacking successive z-index values.
|
||||
/// Each z-index will always be interpreted in the context of its parent z-index.
|
||||
#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)]
|
||||
pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
|
||||
#[derive(Deref, DerefMut, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
|
||||
pub struct StackingOrder {
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
z_indices: SmallVec<[u8; 64]>,
|
||||
}
|
||||
|
||||
impl Default for StackingOrder {
|
||||
fn default() -> Self {
|
||||
StackingOrder {
|
||||
z_indices: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the two different phases when dispatching events.
|
||||
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
@ -235,12 +247,14 @@ pub struct Window {
|
|||
blur_listeners: SubscriberSet<(), AnyObserver>,
|
||||
default_prevented: bool,
|
||||
mouse_position: Point<Pixels>,
|
||||
modifiers: Modifiers,
|
||||
requested_cursor_style: Option<CursorStyle>,
|
||||
scale_factor: f32,
|
||||
bounds: WindowBounds,
|
||||
bounds_observers: SubscriberSet<(), AnyObserver>,
|
||||
active: bool,
|
||||
pub(crate) dirty: bool,
|
||||
pub(crate) drawing: bool,
|
||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
|
||||
|
@ -257,8 +271,8 @@ pub(crate) struct ElementStateBox {
|
|||
// #[derive(Default)]
|
||||
pub(crate) struct Frame {
|
||||
focus: Option<FocusId>,
|
||||
pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
|
||||
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||
mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
pub(crate) scene_builder: SceneBuilder,
|
||||
pub(crate) depth_map: Vec<(StackingOrder, Bounds<Pixels>)>,
|
||||
|
@ -271,8 +285,8 @@ impl Frame {
|
|||
fn new(dispatch_tree: DispatchTree) -> Self {
|
||||
Frame {
|
||||
focus: None,
|
||||
element_states: HashMap::default(),
|
||||
mouse_listeners: HashMap::default(),
|
||||
element_states: FxHashMap::default(),
|
||||
mouse_listeners: FxHashMap::default(),
|
||||
dispatch_tree,
|
||||
scene_builder: SceneBuilder::default(),
|
||||
z_index_stack: StackingOrder::default(),
|
||||
|
@ -313,6 +327,7 @@ impl Window {
|
|||
let display_id = platform_window.display().id();
|
||||
let sprite_atlas = platform_window.sprite_atlas();
|
||||
let mouse_position = platform_window.mouse_position();
|
||||
let modifiers = platform_window.modifiers();
|
||||
let content_size = platform_window.content_size();
|
||||
let scale_factor = platform_window.scale_factor();
|
||||
let bounds = platform_window.bounds();
|
||||
|
@ -376,12 +391,14 @@ impl Window {
|
|||
blur_listeners: SubscriberSet::new(),
|
||||
default_prevented: true,
|
||||
mouse_position,
|
||||
modifiers,
|
||||
requested_cursor_style: None,
|
||||
scale_factor,
|
||||
bounds,
|
||||
bounds_observers: SubscriberSet::new(),
|
||||
active: false,
|
||||
dirty: false,
|
||||
drawing: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
focus: None,
|
||||
|
||||
|
@ -435,7 +452,9 @@ impl<'a> WindowContext<'a> {
|
|||
|
||||
/// Mark the window as dirty, scheduling it to be redrawn on the next frame.
|
||||
pub fn notify(&mut self) {
|
||||
self.window.dirty = true;
|
||||
if !self.window.drawing {
|
||||
self.window.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Close this window.
|
||||
|
@ -805,7 +824,7 @@ impl<'a> WindowContext<'a> {
|
|||
/// a specific need to register a global listener.
|
||||
pub fn on_mouse_event<Event: 'static>(
|
||||
&mut self,
|
||||
handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
|
||||
mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static,
|
||||
) {
|
||||
let order = self.window.next_frame.z_index_stack.clone();
|
||||
self.window
|
||||
|
@ -879,13 +898,18 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.mouse_position
|
||||
}
|
||||
|
||||
/// The current state of the keyboard's modifiers
|
||||
pub fn modifiers(&self) -> Modifiers {
|
||||
self.window.modifiers
|
||||
}
|
||||
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle) {
|
||||
self.window.requested_cursor_style = Some(style)
|
||||
}
|
||||
|
||||
/// Called during painting to invoke the given closure in a new stacking context. The given
|
||||
/// z-index is interpreted relative to the previous call to `stack`.
|
||||
pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
pub fn with_z_index<R>(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.window.next_frame.z_index_stack.push(z_index);
|
||||
let result = f(self);
|
||||
self.window.next_frame.z_index_stack.pop();
|
||||
|
@ -965,14 +989,8 @@ impl<'a> WindowContext<'a> {
|
|||
|
||||
/// Paint one or more quads into the scene for the next frame at the current stacking context.
|
||||
/// Quads are colored rectangular regions with an optional background, border, and corner radius.
|
||||
pub fn paint_quad(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: Corners<Pixels>,
|
||||
background: impl Into<Hsla>,
|
||||
border_widths: Edges<Pixels>,
|
||||
border_color: impl Into<Hsla>,
|
||||
) {
|
||||
/// see [`fill`], [`outline`], and [`quad`] to construct this type.
|
||||
pub fn paint_quad(&mut self, quad: PaintQuad) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let content_mask = self.content_mask();
|
||||
|
||||
|
@ -981,12 +999,12 @@ impl<'a> WindowContext<'a> {
|
|||
&window.next_frame.z_index_stack,
|
||||
Quad {
|
||||
order: 0,
|
||||
bounds: bounds.scale(scale_factor),
|
||||
bounds: quad.bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
background: background.into(),
|
||||
border_color: border_color.into(),
|
||||
corner_radii: corner_radii.scale(scale_factor),
|
||||
border_widths: border_widths.scale(scale_factor),
|
||||
background: quad.background,
|
||||
border_color: quad.border_color,
|
||||
corner_radii: quad.corner_radii.scale(scale_factor),
|
||||
border_widths: quad.border_widths.scale(scale_factor),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1238,6 +1256,10 @@ impl<'a> WindowContext<'a> {
|
|||
|
||||
/// Draw pixels to the display for this window based on the contents of its scene.
|
||||
pub(crate) fn draw(&mut self) -> Scene {
|
||||
let t0 = std::time::Instant::now();
|
||||
self.window.dirty = false;
|
||||
self.window.drawing = true;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
self.window.focus_invalidated = false;
|
||||
|
@ -1325,7 +1347,8 @@ impl<'a> WindowContext<'a> {
|
|||
self.platform.set_cursor_style(cursor_style);
|
||||
}
|
||||
|
||||
self.window.dirty = false;
|
||||
self.window.drawing = false;
|
||||
eprintln!("window draw: {:?}", t0.elapsed());
|
||||
|
||||
scene
|
||||
}
|
||||
|
@ -1342,16 +1365,34 @@ impl<'a> WindowContext<'a> {
|
|||
// API for the mouse position can only occur on the main thread.
|
||||
InputEvent::MouseMove(mouse_move) => {
|
||||
self.window.mouse_position = mouse_move.position;
|
||||
self.window.modifiers = mouse_move.modifiers;
|
||||
InputEvent::MouseMove(mouse_move)
|
||||
}
|
||||
InputEvent::MouseDown(mouse_down) => {
|
||||
self.window.mouse_position = mouse_down.position;
|
||||
self.window.modifiers = mouse_down.modifiers;
|
||||
InputEvent::MouseDown(mouse_down)
|
||||
}
|
||||
InputEvent::MouseUp(mouse_up) => {
|
||||
self.window.mouse_position = mouse_up.position;
|
||||
self.window.modifiers = mouse_up.modifiers;
|
||||
InputEvent::MouseUp(mouse_up)
|
||||
}
|
||||
InputEvent::MouseExited(mouse_exited) => {
|
||||
// todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?")
|
||||
self.window.modifiers = mouse_exited.modifiers;
|
||||
|
||||
InputEvent::MouseExited(mouse_exited)
|
||||
}
|
||||
InputEvent::ModifiersChanged(modifiers_changed) => {
|
||||
self.window.modifiers = modifiers_changed.modifiers;
|
||||
InputEvent::ModifiersChanged(modifiers_changed)
|
||||
}
|
||||
InputEvent::ScrollWheel(scroll_wheel) => {
|
||||
self.window.mouse_position = scroll_wheel.position;
|
||||
self.window.modifiers = scroll_wheel.modifiers;
|
||||
InputEvent::ScrollWheel(scroll_wheel)
|
||||
}
|
||||
// Translate dragging and dropping of external files from the operating system
|
||||
// to internal drag and drop events.
|
||||
InputEvent::FileDrop(file_drop) => match file_drop {
|
||||
|
@ -1359,6 +1400,7 @@ impl<'a> WindowContext<'a> {
|
|||
self.window.mouse_position = position;
|
||||
if self.active_drag.is_none() {
|
||||
self.active_drag = Some(AnyDrag {
|
||||
value: Box::new(files.clone()),
|
||||
view: self.build_view(|_| files).into(),
|
||||
cursor_offset: position,
|
||||
});
|
||||
|
@ -1394,7 +1436,7 @@ impl<'a> WindowContext<'a> {
|
|||
click_count: 1,
|
||||
}),
|
||||
},
|
||||
_ => event,
|
||||
InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
|
||||
};
|
||||
|
||||
if let Some(any_mouse_event) = event.mouse_event() {
|
||||
|
@ -2190,7 +2232,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
&mut self.window_cx
|
||||
}
|
||||
|
||||
pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
pub fn with_z_index<R>(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.window.next_frame.z_index_stack.push(z_index);
|
||||
let result = f(self);
|
||||
self.window.next_frame.z_index_stack.pop();
|
||||
|
@ -2327,10 +2369,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
|||
}
|
||||
|
||||
pub fn notify(&mut self) {
|
||||
self.window_cx.notify();
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
if !self.window.drawing {
|
||||
self.window_cx.notify();
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe_window_bounds(
|
||||
|
@ -2865,12 +2909,12 @@ impl AnyWindowHandle {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl From<SmallVec<[u32; 16]>> for StackingOrder {
|
||||
fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
|
||||
StackingOrder(small_vec)
|
||||
}
|
||||
}
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// impl From<SmallVec<[u32; 16]>> for StackingOrder {
|
||||
// fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
|
||||
// StackingOrder(small_vec)
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ElementId {
|
||||
|
@ -2946,3 +2990,85 @@ impl From<(&'static str, u64)> for ElementId {
|
|||
ElementId::NamedInteger(name.into(), id as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
|
||||
pub struct PaintQuad {
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: Corners<Pixels>,
|
||||
background: Hsla,
|
||||
border_widths: Edges<Pixels>,
|
||||
border_color: Hsla,
|
||||
}
|
||||
|
||||
impl PaintQuad {
|
||||
/// Set the corner radii of the quad.
|
||||
pub fn corner_radii(self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
|
||||
PaintQuad {
|
||||
corner_radii: corner_radii.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the border widths of the quad.
|
||||
pub fn border_widths(self, border_widths: impl Into<Edges<Pixels>>) -> Self {
|
||||
PaintQuad {
|
||||
border_widths: border_widths.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the border color of the quad.
|
||||
pub fn border_color(self, border_color: impl Into<Hsla>) -> Self {
|
||||
PaintQuad {
|
||||
border_color: border_color.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the background color of the quad.
|
||||
pub fn background(self, background: impl Into<Hsla>) -> Self {
|
||||
PaintQuad {
|
||||
background: background.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a quad with the given parameters.
|
||||
pub fn quad(
|
||||
bounds: Bounds<Pixels>,
|
||||
corner_radii: impl Into<Corners<Pixels>>,
|
||||
background: impl Into<Hsla>,
|
||||
border_widths: impl Into<Edges<Pixels>>,
|
||||
border_color: impl Into<Hsla>,
|
||||
) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds,
|
||||
corner_radii: corner_radii.into(),
|
||||
background: background.into(),
|
||||
border_widths: border_widths.into(),
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filled quad with the given bounds and background color.
|
||||
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
background: background.into(),
|
||||
border_widths: (0.).into(),
|
||||
border_color: transparent_black(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rectangle outline with the given bounds, border color, and a 1px border width
|
||||
pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>) -> PaintQuad {
|
||||
PaintQuad {
|
||||
bounds: bounds.into(),
|
||||
corner_radii: (0.).into(),
|
||||
background: transparent_black(),
|
||||
border_widths: (1.).into(),
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
}
|
||||
|
|
23
crates/gpui2_macros/src/derive_render.rs
Normal file
23
crates/gpui2_macros/src/derive_render.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let type_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let gen = quote! {
|
||||
impl #impl_generics gpui::Render for #type_name #type_generics
|
||||
#where_clause
|
||||
{
|
||||
type Element = ();
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen.into()
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod derive_into_element;
|
||||
mod derive_render;
|
||||
mod register_action;
|
||||
mod style_helpers;
|
||||
mod test;
|
||||
|
@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
|
|||
derive_into_element::derive_into_element(input)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Render)]
|
||||
pub fn derive_render(input: TokenStream) -> TokenStream {
|
||||
derive_render::derive_render(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn style_helpers(input: TokenStream) -> TokenStream {
|
||||
style_helpers::style_helpers(input)
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
|
||||
"version": "1.0.12"
|
||||
"revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50",
|
||||
"version": "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,8 +24,8 @@
|
|||
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
|
||||
"version": "104.5112.17"
|
||||
"revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08",
|
||||
"version": "114.5735.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ let package = Package(
|
|||
targets: ["LiveKitBridge"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
|
||||
"version": "1.0.12"
|
||||
"revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50",
|
||||
"version": "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,8 +24,8 @@
|
|||
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
|
||||
"version": "104.5112.17"
|
||||
"revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08",
|
||||
"version": "114.5735.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ let package = Package(
|
|||
targets: ["LiveKitBridge2"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
|
||||
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
|
|
|
@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
|
|||
settings = { package = "settings2", path = "../settings2" }
|
||||
util = { path = "../util" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2"}
|
||||
|
||||
parking_lot.workspace = true
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, rems, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
|
||||
MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
|
||||
WindowContext,
|
||||
div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
|
||||
FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task,
|
||||
UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
pub delegate: D,
|
||||
|
@ -13,6 +14,7 @@ pub struct Picker<D: PickerDelegate> {
|
|||
editor: View<Editor>,
|
||||
pending_update_matches: Option<Task<()>>,
|
||||
confirm_on_update: Option<bool>,
|
||||
width: Option<Length>,
|
||||
}
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
|
@ -55,11 +57,17 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
scroll_handle: UniformListScrollHandle::new(),
|
||||
pending_update_matches: None,
|
||||
confirm_on_update: None,
|
||||
width: None,
|
||||
};
|
||||
this.update_matches("".to_string(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
|
||||
self.width = Some(width.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn focus(&self, cx: &mut WindowContext) {
|
||||
self.editor.update(cx, |editor, cx| editor.focus(cx));
|
||||
}
|
||||
|
@ -113,8 +121,9 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.delegate.dismissed(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
|
@ -146,9 +155,15 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
event: &editor::EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let editor::EditorEvent::BufferEdited = event {
|
||||
let query = self.editor.read(cx).text(cx);
|
||||
self.update_matches(query, cx);
|
||||
match event {
|
||||
editor::EditorEvent::BufferEdited => {
|
||||
let query = self.editor.read(cx).text(cx);
|
||||
self.update_matches(query, cx);
|
||||
}
|
||||
editor::EditorEvent::Blurred => {
|
||||
self.cancel(&menu::Cancel, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,6 +204,9 @@ impl<D: PickerDelegate> Picker<D> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
|
||||
impl<D: PickerDelegate> ModalView for Picker<D> {}
|
||||
|
||||
impl<D: PickerDelegate> Render for Picker<D> {
|
||||
type Element = Div;
|
||||
|
||||
|
@ -212,6 +230,9 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||
div()
|
||||
.key_context("picker")
|
||||
.size_full()
|
||||
.when_some(self.width, |el, width| {
|
||||
el.w(width)
|
||||
})
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
|
@ -262,7 +283,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
|||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.p_1()
|
||||
)
|
||||
.max_h_72()
|
||||
.overflow_hidden(),
|
||||
|
|
|
@ -1377,33 +1377,28 @@ impl ProjectPanel {
|
|||
})
|
||||
.unwrap_or(theme.status().info);
|
||||
|
||||
let file_name = details.filename.clone();
|
||||
let icon = details.icon.clone();
|
||||
let depth = details.depth;
|
||||
div()
|
||||
.id(entry_id.to_proto() as usize)
|
||||
.on_drag({
|
||||
let details = details.clone();
|
||||
move |cx| {
|
||||
let details = details.clone();
|
||||
cx.build_view(|_| DraggedProjectEntryView {
|
||||
details,
|
||||
width,
|
||||
entry_id,
|
||||
})
|
||||
}
|
||||
.on_drag(entry_id, move |entry_id, cx| {
|
||||
cx.build_view(|_| DraggedProjectEntryView {
|
||||
details: details.clone(),
|
||||
width,
|
||||
entry_id: *entry_id,
|
||||
})
|
||||
})
|
||||
.drag_over::<DraggedProjectEntryView>(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
move |this, dragged_view: &View<DraggedProjectEntryView>, cx| {
|
||||
this.move_entry(dragged_view.read(cx).entry_id, entry_id, kind.is_file(), cx);
|
||||
},
|
||||
))
|
||||
.drag_over::<ProjectEntryId>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
|
||||
this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(entry_id.to_proto() as usize)
|
||||
.indent_level(details.depth)
|
||||
.indent_level(depth)
|
||||
.indent_step_size(px(settings.indent_size))
|
||||
.selected(is_selected)
|
||||
.child(if let Some(icon) = &details.icon {
|
||||
.child(if let Some(icon) = &icon {
|
||||
div().child(IconElement::from_path(icon.to_string()))
|
||||
} else {
|
||||
div()
|
||||
|
@ -1414,7 +1409,7 @@ impl ProjectPanel {
|
|||
} else {
|
||||
div()
|
||||
.text_color(filename_text_color)
|
||||
.child(Label::new(details.filename.clone()))
|
||||
.child(Label::new(file_name))
|
||||
}
|
||||
.ml_1(),
|
||||
)
|
||||
|
|
37
crates/project_symbols2/Cargo.toml
Normal file
37
crates/project_symbols2/Cargo.toml
Normal file
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "project_symbols2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/project_symbols.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = {package = "gpui2", path = "../gpui2" }
|
||||
picker = {package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
text = {package = "text2", path = "../text2" }
|
||||
settings = {package = "settings2", path = "../settings2" }
|
||||
workspace = {package = "workspace2", path = "../workspace2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow.workspace = true
|
||||
ordered-float.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
futures.workspace = true
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
411
crates/project_symbols2/src/project_symbols.rs
Normal file
411
crates/project_symbols2/src/project_symbols.rs
Normal file
|
@ -0,0 +1,411 @@
|
|||
use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task,
|
||||
View, ViewContext, WeakView,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, Symbol};
|
||||
use std::{borrow::Cow, cmp::Reverse, sync::Arc};
|
||||
use theme::ActiveTheme;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(project_symbols, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let handle = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
let delegate = ProjectSymbolsDelegate::new(handle, project);
|
||||
Picker::new(delegate, cx).width(rems(34.))
|
||||
})
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub type ProjectSymbols = View<Picker<ProjectSymbolsDelegate>>;
|
||||
|
||||
pub struct ProjectSymbolsDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
selected_match_index: usize,
|
||||
symbols: Vec<Symbol>,
|
||||
visible_match_candidates: Vec<StringMatchCandidate>,
|
||||
external_match_candidates: Vec<StringMatchCandidate>,
|
||||
show_worktree_root_name: bool,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
|
||||
impl ProjectSymbolsDelegate {
|
||||
fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
project,
|
||||
selected_match_index: 0,
|
||||
symbols: Default::default(),
|
||||
visible_match_candidates: Default::default(),
|
||||
external_match_candidates: Default::default(),
|
||||
matches: Default::default(),
|
||||
show_worktree_root_name: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) {
|
||||
const MAX_MATCHES: usize = 100;
|
||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&self.visible_match_candidates,
|
||||
query,
|
||||
false,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&self.external_match_candidates,
|
||||
query,
|
||||
false,
|
||||
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let sort_key_for_match = |mat: &StringMatch| {
|
||||
let symbol = &self.symbols[mat.candidate_id];
|
||||
(
|
||||
Reverse(OrderedFloat(mat.score)),
|
||||
&symbol.label.text[symbol.label.filter_range.clone()],
|
||||
)
|
||||
};
|
||||
|
||||
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
let mut matches = visible_matches;
|
||||
matches.append(&mut external_matches);
|
||||
|
||||
for mat in &mut matches {
|
||||
let symbol = &self.symbols[mat.candidate_id];
|
||||
let filter_start = symbol.label.filter_range.start;
|
||||
for position in &mut mat.positions {
|
||||
*position += filter_start;
|
||||
}
|
||||
}
|
||||
|
||||
self.matches = matches;
|
||||
self.set_selected_index(0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ProjectSymbolsDelegate {
|
||||
type ListItem = ListItem;
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search project symbols...".into()
|
||||
}
|
||||
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(symbol) = self
|
||||
.matches
|
||||
.get(self.selected_match_index)
|
||||
.map(|mat| self.symbols[mat.candidate_id].clone())
|
||||
{
|
||||
let buffer = self.project.update(cx, |project, cx| {
|
||||
project.open_buffer_for_symbol(&symbol, cx)
|
||||
});
|
||||
let symbol = symbol.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let position = buffer
|
||||
.read(cx)
|
||||
.clip_point_utf16(symbol.range.start, Bias::Left);
|
||||
|
||||
let editor = if secondary {
|
||||
workspace.split_project_item::<Editor>(buffer, cx)
|
||||
} else {
|
||||
workspace.open_project_item::<Editor>(buffer, cx)
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
});
|
||||
})?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_match_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_match_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
self.filter(&query, cx);
|
||||
self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
|
||||
let symbols = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.symbols(&query, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let symbols = symbols.await.log_err();
|
||||
if let Some(symbols) = symbols {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let delegate = &mut this.delegate;
|
||||
let project = delegate.project.read(cx);
|
||||
let (visible_match_candidates, external_match_candidates) = symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
symbol.label.text[symbol.label.filter_range.clone()].to_string(),
|
||||
)
|
||||
})
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
});
|
||||
|
||||
delegate.visible_match_candidates = visible_match_candidates;
|
||||
delegate.external_match_candidates = external_match_candidates;
|
||||
delegate.symbols = symbols;
|
||||
delegate.filter(&query, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let string_match = &self.matches[ix];
|
||||
let symbol = &self.symbols[string_match.candidate_id];
|
||||
let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
|
||||
|
||||
let mut path = symbol.path.path.to_string_lossy();
|
||||
if self.show_worktree_root_name {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
|
||||
path = Cow::Owned(format!(
|
||||
"{}{}{}",
|
||||
worktree.read(cx).root_name(),
|
||||
std::path::MAIN_SEPARATOR,
|
||||
path.as_ref()
|
||||
));
|
||||
}
|
||||
}
|
||||
let label = symbol.label.text.clone();
|
||||
let path = path.to_string().clone();
|
||||
|
||||
let highlights = gpui::combine_highlights(
|
||||
string_match
|
||||
.positions
|
||||
.iter()
|
||||
.map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())),
|
||||
syntax_runs.map(|(range, mut highlight)| {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
(range, highlight)
|
||||
}),
|
||||
);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
// todo!() combine_syntax_and_fuzzy_match_highlights()
|
||||
v_stack()
|
||||
.child(
|
||||
LabelLike::new().child(
|
||||
StyledText::new(label)
|
||||
.with_highlights(&cx.text_style().clone(), highlights),
|
||||
),
|
||||
)
|
||||
.child(Label::new(path).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{serde_json::json, TestAppContext, VisualContext};
|
||||
use language::{FakeLspAdapter, Language, LanguageConfig};
|
||||
use project::FakeFs;
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_symbols(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set up fake language server to return fuzzy matches against
|
||||
// a fixed set of symbol names.
|
||||
let fake_symbols = [
|
||||
symbol("one", "/external"),
|
||||
symbol("ton", "/dir/test.rs"),
|
||||
symbol("uno", "/dir/test.rs"),
|
||||
];
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
|
||||
move |params: lsp::WorkspaceSymbolParams, cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
let fake_symbols = fake_symbols.clone();
|
||||
async move {
|
||||
let candidates = fake_symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if params.query.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
¶ms.query,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| fake_symbols[mat.candidate_id].clone())
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create the project symbols view.
|
||||
let symbols = cx.build_view(|cx| {
|
||||
Picker::new(
|
||||
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Spawn multiples updates before the first update completes,
|
||||
// such that in the end, there are no matches. Testing for regression:
|
||||
// https://github.com/zed-industries/zed/issues/861
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("o".to_string(), cx);
|
||||
p.update_matches("on".to_string(), cx);
|
||||
p.update_matches("onex".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||
});
|
||||
|
||||
// Spawn more updates such that in the end, there are matches.
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("one".to_string(), cx);
|
||||
p.update_matches("on".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
let delegate = &symbols.delegate;
|
||||
assert_eq!(delegate.matches.len(), 2);
|
||||
assert_eq!(delegate.matches[0].string, "ton");
|
||||
assert_eq!(delegate.matches[1].string, "one");
|
||||
});
|
||||
|
||||
// Spawn more updates such that in the end, there are again no matches.
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("o".to_string(), cx);
|
||||
p.update_matches("".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: name.to_string(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
tags: None,
|
||||
deprecated: None,
|
||||
container_name: None,
|
||||
location: lsp::Location::new(
|
||||
lsp::Url::from_file_path(path.as_ref()).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,14 +23,15 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.observe_new_views(RecentProjects::register).detach();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RecentProjects {
|
||||
picker: View<Picker<RecentProjectsDelegate>>,
|
||||
pub picker: View<Picker<RecentProjectsDelegate>>,
|
||||
}
|
||||
|
||||
impl ModalView for RecentProjects {}
|
||||
|
||||
impl RecentProjects {
|
||||
fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||
fn new(delegate: RecentProjectsDelegate, cx: &mut WindowContext<'_>) -> Self {
|
||||
Self {
|
||||
picker: cx.build_view(|cx| Picker::new(delegate, cx)),
|
||||
}
|
||||
|
@ -86,6 +87,16 @@ impl RecentProjects {
|
|||
Ok(())
|
||||
}))
|
||||
}
|
||||
pub fn open_popover(
|
||||
workspace: WeakView<Workspace>,
|
||||
workspaces: Vec<WorkspaceLocation>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
RecentProjectsDelegate::new(workspace, workspaces, false),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RecentProjects {}
|
||||
|
@ -127,7 +138,7 @@ impl RecentProjectsDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
||||
impl PickerDelegate for RecentProjectsDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
|
@ -202,11 +213,11 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.dismissed(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
|
|
|
@ -23,7 +23,7 @@ use util::ResultExt;
|
|||
use workspace::{
|
||||
item::ItemHandle,
|
||||
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
|
@ -456,6 +456,9 @@ impl BufferSearchBar {
|
|||
cx.focus(&handle);
|
||||
}
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
ToolbarItemLocation::Hidden,
|
||||
));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -488,6 +491,9 @@ impl BufferSearchBar {
|
|||
self.dismissed = false;
|
||||
cx.notify();
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
ToolbarItemLocation::Secondary,
|
||||
));
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
@ -10,11 +10,13 @@ use editor::{
|
|||
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent,
|
||||
MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
|
||||
};
|
||||
use editor::{EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
actions, div, white, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
|
||||
EventEmitter, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, ModelContext,
|
||||
ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
|
||||
actions, div, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
|
||||
EventEmitter, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement,
|
||||
KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled,
|
||||
Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use project::{
|
||||
|
@ -23,6 +25,7 @@ use project::{
|
|||
};
|
||||
use semantic_index::{SemanticIndex, SemanticIndexStatus};
|
||||
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
|
@ -32,6 +35,7 @@ use std::{
|
|||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use ui::{
|
||||
h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon,
|
||||
|
@ -425,7 +429,12 @@ impl Item for ProjectSearchView {
|
|||
.filter(|query| !query.is_empty())
|
||||
.unwrap_or_else(|| "Project search".into());
|
||||
h_stack()
|
||||
.child(IconElement::new(Icon::MagnifyingGlass))
|
||||
.gap_2()
|
||||
.child(IconElement::new(Icon::MagnifyingGlass).color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.child(Label::new(tab_name).color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
|
@ -1428,6 +1437,36 @@ impl ProjectSearchBar {
|
|||
};
|
||||
new_placeholder_text
|
||||
}
|
||||
|
||||
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if editor.read(cx).read_only() {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features,
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3).into(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectSearchBar {
|
||||
|
@ -1448,122 +1487,62 @@ impl Render for ProjectSearchBar {
|
|||
}
|
||||
let search = search.read(cx);
|
||||
let semantic_is_available = SemanticIndex::enabled(cx);
|
||||
let query_column = v_stack()
|
||||
//.flex_1()
|
||||
.child(
|
||||
h_stack()
|
||||
.min_w_80()
|
||||
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
|
||||
.on_action(
|
||||
cx.listener(|this, action, cx| this.previous_history_query(action, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
|
||||
.child(IconElement::new(Icon::MagnifyingGlass))
|
||||
.child(search.query_editor.clone())
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
IconButton::new("project-search-filter-button", Icon::Filter)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_filters(cx);
|
||||
}))
|
||||
.selected(
|
||||
self.active_project_search
|
||||
.as_ref()
|
||||
.map(|search| search.read(cx).filters_enabled)
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.when(search.current_mode != SearchMode::Semantic, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"project-search-case-sensitive",
|
||||
Icon::CaseSensitive,
|
||||
)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Toggle case sensitive",
|
||||
&ToggleCaseSensitive,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.selected(
|
||||
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
|
||||
)
|
||||
.on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
cx,
|
||||
);
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("project-search-whole-word", Icon::WholeWord)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Toggle whole word",
|
||||
&ToggleWholeWord,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.selected(
|
||||
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::WHOLE_WORD,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.border_2()
|
||||
.bg(white())
|
||||
.rounded_lg(),
|
||||
)
|
||||
.when(search.filters_enabled, |this| {
|
||||
this.child(
|
||||
let query_column = v_stack().child(
|
||||
h_stack()
|
||||
.min_w(rems(512. / 16.))
|
||||
.px_2()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_lg()
|
||||
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
|
||||
.on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
|
||||
.on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
|
||||
.child(IconElement::new(Icon::MagnifyingGlass))
|
||||
.child(self.render_text_input(&search.query_editor, cx))
|
||||
.child(
|
||||
h_stack()
|
||||
.mt_2()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.border_1()
|
||||
.mr_2()
|
||||
.child(search.included_files_editor.clone())
|
||||
.when(search.current_mode != SearchMode::Semantic, |this| {
|
||||
this.child(
|
||||
SearchOptions::INCLUDE_IGNORED.as_button(
|
||||
search
|
||||
.search_options
|
||||
.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
cx.listener(|this, _, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
IconButton::new("project-search-filter-button", Icon::Filter)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.toggle_filters(cx);
|
||||
}))
|
||||
.selected(
|
||||
self.active_project_search
|
||||
.as_ref()
|
||||
.map(|search| search.read(cx).filters_enabled)
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.border_1()
|
||||
.ml_2()
|
||||
.child(search.excluded_files_editor.clone()),
|
||||
),
|
||||
)
|
||||
});
|
||||
.when(search.current_mode != SearchMode::Semantic, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"project-search-case-sensitive",
|
||||
Icon::CaseSensitive,
|
||||
)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Toggle case sensitive",
|
||||
&ToggleCaseSensitive,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
|
||||
.on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
let mode_column = v_stack().items_start().justify_start().child(
|
||||
h_stack()
|
||||
.child(
|
||||
|
@ -1619,12 +1598,16 @@ impl Render for ProjectSearchBar {
|
|||
);
|
||||
let replace_column = if search.replace_enabled {
|
||||
h_stack()
|
||||
.p_1()
|
||||
.flex_1()
|
||||
.border_2()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_lg()
|
||||
.child(IconElement::new(Icon::Replace).size(ui::IconSize::Small))
|
||||
.child(search.replacement_editor.clone())
|
||||
.child(self.render_text_input(&search.replacement_editor, cx))
|
||||
} else {
|
||||
// Fill out the space if we don't have a replacement editor.
|
||||
h_stack().flex_1()
|
||||
|
@ -1685,12 +1668,12 @@ impl Render for ProjectSearchBar {
|
|||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
|
||||
]);
|
||||
h_stack()
|
||||
v_stack()
|
||||
.key_context(key_context)
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.p_1()
|
||||
.m_2()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.on_action(cx.listener(|this, _: &ToggleFilters, cx| {
|
||||
this.toggle_filters(cx);
|
||||
}))
|
||||
|
@ -1742,10 +1725,59 @@ impl Render for ProjectSearchBar {
|
|||
}))
|
||||
})
|
||||
})
|
||||
.child(query_column)
|
||||
.child(mode_column)
|
||||
.child(replace_column)
|
||||
.child(actions_column)
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(query_column)
|
||||
.child(mode_column)
|
||||
.child(replace_column)
|
||||
.child(actions_column),
|
||||
)
|
||||
.when(search.filters_enabled, |this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_lg()
|
||||
.child(self.render_text_input(&search.included_files_editor, cx))
|
||||
.when(search.current_mode != SearchMode::Semantic, |this| {
|
||||
this.child(
|
||||
SearchOptions::INCLUDE_IGNORED.as_button(
|
||||
search
|
||||
.search_options
|
||||
.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
cx.listener(|this, _, cx| {
|
||||
this.toggle_search_option(
|
||||
SearchOptions::INCLUDE_IGNORED,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_lg()
|
||||
.child(self.render_text_input(&search.excluded_files_editor, cx)),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
// impl Entity for ProjectSearchBar {
|
||||
|
|
|
@ -12,9 +12,10 @@ path = "src/storybook2.rs"
|
|||
anyhow.workspace = true
|
||||
# TODO: Remove after diagnosing stack overflow.
|
||||
backtrace-on-stack-overflow = "0.3.0"
|
||||
clap = { version = "4.4", features = ["derive", "string"] }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4", features = ["derive", "string"] }
|
||||
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
itertools = "0.11.0"
|
||||
|
|
|
@ -78,7 +78,7 @@ impl Styles for Div {}
|
|||
|
||||
#[derive(IntoElement)]
|
||||
struct ZIndexExample {
|
||||
z_index: u32,
|
||||
z_index: u8,
|
||||
}
|
||||
|
||||
impl RenderOnce for ZIndexExample {
|
||||
|
@ -170,7 +170,7 @@ impl RenderOnce for ZIndexExample {
|
|||
}
|
||||
|
||||
impl ZIndexExample {
|
||||
pub fn new(z_index: u32) -> Self {
|
||||
pub fn new(z_index: u8) -> Self {
|
||||
Self { z_index }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ mod story_selector;
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use dialoguer::FuzzySelect;
|
||||
use gpui::{
|
||||
div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext,
|
||||
WindowBounds, WindowOptions,
|
||||
|
@ -12,11 +13,12 @@ use gpui::{
|
|||
use log::LevelFilter;
|
||||
use settings2::{default_settings, Settings, SettingsStore};
|
||||
use simplelog::SimpleLogger;
|
||||
use strum::IntoEnumIterator;
|
||||
use theme2::{ThemeRegistry, ThemeSettings};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::assets::Assets;
|
||||
use crate::story_selector::StorySelector;
|
||||
use crate::story_selector::{ComponentStory, StorySelector};
|
||||
|
||||
// gpui::actions! {
|
||||
// storybook,
|
||||
|
@ -43,7 +45,17 @@ fn main() {
|
|||
|
||||
let args = Args::parse();
|
||||
|
||||
let story_selector = args.story.clone();
|
||||
let story_selector = args.story.clone().unwrap_or_else(|| {
|
||||
let stories = ComponentStory::iter().collect::<Vec<_>>();
|
||||
|
||||
let selection = FuzzySelect::new()
|
||||
.with_prompt("Choose a story to run:")
|
||||
.items(&stories)
|
||||
.interact()
|
||||
.unwrap();
|
||||
|
||||
StorySelector::Component(stories[selection])
|
||||
});
|
||||
let theme_name = args.theme.unwrap_or("One Dark".to_string());
|
||||
|
||||
let asset_source = Arc::new(Assets);
|
||||
|
@ -58,7 +70,7 @@ fn main() {
|
|||
|
||||
theme2::init(theme2::LoadThemes::All, cx);
|
||||
|
||||
let selector = story_selector.unwrap_or(StorySelector::KitchenSink);
|
||||
let selector = story_selector;
|
||||
|
||||
let theme_registry = cx.global::<ThemeRegistry>();
|
||||
let mut theme_settings = ThemeSettings::get_global(cx).clone();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
|
||||
AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
|
||||
FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
|
||||
black, div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
|
||||
Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, Interactivity,
|
||||
IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
|
||||
PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
|
||||
TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
|
||||
|
@ -133,13 +133,7 @@ impl LayoutRect {
|
|||
)
|
||||
.into();
|
||||
|
||||
cx.paint_quad(
|
||||
Bounds::new(position, size),
|
||||
Default::default(),
|
||||
self.color,
|
||||
Default::default(),
|
||||
transparent_black(),
|
||||
);
|
||||
cx.paint_quad(fill(Bounds::new(position, size), self.color));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,11 +145,11 @@ pub struct TerminalElement {
|
|||
focused: bool,
|
||||
cursor_visible: bool,
|
||||
can_navigate_to_selected_word: bool,
|
||||
interactivity: gpui::Interactivity,
|
||||
interactivity: Interactivity,
|
||||
}
|
||||
|
||||
impl InteractiveElement for TerminalElement {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -611,141 +605,156 @@ impl TerminalElement {
|
|||
}
|
||||
|
||||
fn register_mouse_listeners(
|
||||
self,
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
mode: TermMode,
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
) {
|
||||
let focus = self.focus.clone();
|
||||
let connection = self.terminal.clone();
|
||||
let terminal = self.terminal.clone();
|
||||
|
||||
let mut this = self
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let connection = connection.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
cx.focus(&focus);
|
||||
//todo!(context menu)
|
||||
// v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
|
||||
connection.update(cx, |terminal, cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
self.interactivity.on_mouse_down(MouseButton::Left, {
|
||||
let terminal = terminal.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
cx.focus(&focus);
|
||||
//todo!(context menu)
|
||||
// v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
self.interactivity.on_mouse_move({
|
||||
let terminal = terminal.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.mouse_drag(e, origin, bounds);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_mouse_move({
|
||||
let connection = connection.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
|
||||
connection.update(cx, |terminal, cx| {
|
||||
terminal.mouse_drag(e, origin, bounds);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
self.interactivity.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_click({
|
||||
let terminal = terminal.clone();
|
||||
move |e, cx| {
|
||||
if e.down.button == MouseButton::Right {
|
||||
let mouse_mode = terminal.update(cx, |terminal, _cx| {
|
||||
terminal.mouse_mode(e.down.modifiers.shift)
|
||||
});
|
||||
|
||||
if !mouse_mode {
|
||||
//todo!(context menu)
|
||||
// view.deploy_context_menu(e.position, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
}
|
||||
});
|
||||
|
||||
self.interactivity.on_mouse_move({
|
||||
let terminal = terminal.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
if focus.is_focused(cx) {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.mouse_move(&e, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
self.interactivity.on_scroll_wheel({
|
||||
let terminal = terminal.clone();
|
||||
move |e, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.scroll_wheel(e, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
self.interactivity.on_drop::<ExternalPaths>({
|
||||
let focus = focus.clone();
|
||||
let terminal = terminal.clone();
|
||||
move |external_paths, cx| {
|
||||
cx.focus(&focus);
|
||||
let mut new_text = external_paths
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| format!(" {path:?}"))
|
||||
.join("");
|
||||
new_text.push(' ');
|
||||
terminal.update(cx, |terminal, _| {
|
||||
// todo!() long paths are not displayed properly albeit the text is there
|
||||
terminal.paste(&new_text);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse mode handlers:
|
||||
// All mouse modes need the extra click handlers
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
self.interactivity.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection.clone(),
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
);
|
||||
self.interactivity.on_mouse_up(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let connection = connection.clone();
|
||||
move |e, cx| {
|
||||
if e.down.button == MouseButton::Right {
|
||||
let mouse_mode = connection.update(cx, |terminal, _cx| {
|
||||
terminal.mouse_mode(e.down.modifiers.shift)
|
||||
});
|
||||
|
||||
if !mouse_mode {
|
||||
//todo!(context menu)
|
||||
// view.deploy_context_menu(e.position, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_mouse_move({
|
||||
let connection = connection.clone();
|
||||
let focus = focus.clone();
|
||||
move |e, cx| {
|
||||
if focus.is_focused(cx) {
|
||||
connection.update(cx, |terminal, cx| {
|
||||
terminal.mouse_move(&e, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_scroll_wheel({
|
||||
let connection = connection.clone();
|
||||
move |e, cx| {
|
||||
connection.update(cx, |terminal, cx| {
|
||||
terminal.scroll_wheel(e, origin);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse mode handlers:
|
||||
// All mouse modes need the extra click handlers
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
this = this
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_mouse_up(
|
||||
MouseButton::Right,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection.clone(),
|
||||
origin,
|
||||
focus.clone(),
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_mouse_up(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
focus,
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
self.interactivity.on_mouse_up(
|
||||
MouseButton::Middle,
|
||||
TerminalElement::generic_button_handler(
|
||||
terminal,
|
||||
origin,
|
||||
focus,
|
||||
move |terminal, origin, e, cx| {
|
||||
terminal.mouse_up(&e, origin, cx);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -770,18 +779,17 @@ impl Element for TerminalElement {
|
|||
(layout_id, interactive_state)
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext<'_>) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::State,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) {
|
||||
let mut layout = self.compute_layout(bounds, cx);
|
||||
|
||||
let theme = cx.theme();
|
||||
|
||||
cx.paint_quad(
|
||||
bounds,
|
||||
Default::default(),
|
||||
layout.background_color,
|
||||
Default::default(),
|
||||
Hsla::default(),
|
||||
);
|
||||
cx.paint_quad(fill(bounds, layout.background_color));
|
||||
let origin = bounds.origin + Point::new(layout.gutter, px(0.));
|
||||
|
||||
let terminal_input_handler = TerminalInputHandler {
|
||||
|
@ -795,33 +803,19 @@ impl Element for TerminalElement {
|
|||
|
||||
let terminal_focus_handle = self.focus.clone();
|
||||
let terminal_handle = self.terminal.clone();
|
||||
let mut this: TerminalElement = self
|
||||
.register_mouse_listeners(origin, layout.mode, bounds, cx)
|
||||
.drag_over::<ExternalPaths>(|style| {
|
||||
// todo!() why does not it work? z-index of elements?
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
})
|
||||
.on_drop::<ExternalPaths>(move |external_paths, cx| {
|
||||
cx.focus(&terminal_focus_handle);
|
||||
let mut new_text = external_paths
|
||||
.read(cx)
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| format!(" {path:?}"))
|
||||
.join("");
|
||||
new_text.push(' ');
|
||||
terminal_handle.update(cx, |terminal, _| {
|
||||
// todo!() long paths are not displayed properly albeit the text is there
|
||||
terminal.paste(&new_text);
|
||||
});
|
||||
});
|
||||
self.register_mouse_listeners(origin, layout.mode, bounds, cx);
|
||||
|
||||
let interactivity = mem::take(&mut this.interactivity);
|
||||
// todo!(change this to work in terms of on_drag_move or some such)
|
||||
// .drag_over::<ExternalPaths>(|style| {
|
||||
// // todo!() why does not it work? z-index of elements?
|
||||
// style.bg(cx.theme().colors().ghost_element_hover)
|
||||
// })
|
||||
|
||||
let mut interactivity = mem::take(&mut self.interactivity);
|
||||
interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| {
|
||||
cx.handle_input(&this.focus, terminal_input_handler);
|
||||
cx.handle_input(&self.focus, terminal_input_handler);
|
||||
|
||||
this.register_key_listeners(cx);
|
||||
self.register_key_listeners(cx);
|
||||
|
||||
for rect in &layout.rects {
|
||||
rect.paint(origin, &layout, cx);
|
||||
|
@ -852,7 +846,7 @@ impl Element for TerminalElement {
|
|||
}
|
||||
});
|
||||
|
||||
if this.cursor_visible {
|
||||
if self.cursor_visible {
|
||||
cx.with_z_index(3, |cx| {
|
||||
if let Some(cursor) = &layout.cursor {
|
||||
cursor.paint(origin, cx);
|
||||
|
@ -860,7 +854,7 @@ impl Element for TerminalElement {
|
|||
});
|
||||
}
|
||||
|
||||
if let Some(element) = layout.hyperlink_tooltip.take() {
|
||||
if let Some(mut element) = layout.hyperlink_tooltip.take() {
|
||||
let width: AvailableSpace = bounds.size.width.into();
|
||||
let height: AvailableSpace = bounds.size.height.into();
|
||||
element.draw(origin, Size { width, height }, cx)
|
||||
|
|
|
@ -47,6 +47,7 @@ pub fn andromeda() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x3a3f4c77).into()),
|
||||
scrollbar_track_background: Some(rgba(0x23262eff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x1b1d23ff).into()),
|
||||
editor_foreground: Some(rgba(0xd5ced9ff).into()),
|
||||
editor_background: Some(rgba(0x23262eff).into()),
|
||||
editor_gutter_background: Some(rgba(0x23262eff).into()),
|
||||
editor_line_number: Some(rgba(0x746f77ff).into()),
|
||||
|
@ -261,6 +262,7 @@ pub fn andromeda() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x3a3f4c77).into()),
|
||||
scrollbar_track_background: Some(rgba(0x262a33ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x1b1d23ff).into()),
|
||||
editor_foreground: Some(rgba(0xd5ced9ff).into()),
|
||||
editor_background: Some(rgba(0x262a33ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x262a33ff).into()),
|
||||
editor_line_number: Some(rgba(0x746f77ff).into()),
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn ayu() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x8a919966).into()),
|
||||
scrollbar_track_background: Some(rgba(0xf8f9faff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x6b7d8f1f).into()),
|
||||
editor_foreground: Some(rgba(0x5c6166ff).into()),
|
||||
editor_background: Some(rgba(0xf8f9faff).into()),
|
||||
editor_gutter_background: Some(rgba(0xf8f9faff).into()),
|
||||
editor_line_number: Some(rgba(0x8a919966).into()),
|
||||
|
@ -352,6 +353,7 @@ pub fn ayu() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x707a8c66).into()),
|
||||
scrollbar_track_background: Some(rgba(0x1f2430ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x171b24ff).into()),
|
||||
editor_foreground: Some(rgba(0xcccac2ff).into()),
|
||||
editor_background: Some(rgba(0x1f2430ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x1f2430ff).into()),
|
||||
editor_line_number: Some(rgba(0x8a919966).into()),
|
||||
|
@ -658,6 +660,7 @@ pub fn ayu() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x565b6666).into()),
|
||||
scrollbar_track_background: Some(rgba(0x0b0e14ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x1e232bff).into()),
|
||||
editor_foreground: Some(rgba(0xbfbdb6ff).into()),
|
||||
editor_background: Some(rgba(0x0b0e14ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x0b0e14ff).into()),
|
||||
editor_line_number: Some(rgba(0x6c738099).into()),
|
||||
|
|
|
@ -43,6 +43,7 @@ pub fn dracula() -> UserThemeFamily {
|
|||
tab_active_background: Some(rgba(0x282a36ff).into()),
|
||||
scrollbar_track_background: Some(rgba(0x282a36ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x191a21ff).into()),
|
||||
editor_foreground: Some(rgba(0xf8f8f2ff).into()),
|
||||
editor_background: Some(rgba(0x282a36ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x282a36ff).into()),
|
||||
editor_line_number: Some(rgba(0x6272a4ff).into()),
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x50494599).into()),
|
||||
scrollbar_track_background: Some(rgba(0x1d2021ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x1d202100).into()),
|
||||
editor_foreground: Some(rgba(0xebdbb2ff).into()),
|
||||
editor_background: Some(rgba(0x1d2021ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x1d2021ff).into()),
|
||||
editor_line_number: Some(rgba(0x665c54ff).into()),
|
||||
|
@ -338,6 +339,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x50494599).into()),
|
||||
scrollbar_track_background: Some(rgba(0x282828ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x28282800).into()),
|
||||
editor_foreground: Some(rgba(0xebdbb2ff).into()),
|
||||
editor_background: Some(rgba(0x282828ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x282828ff).into()),
|
||||
editor_line_number: Some(rgba(0x665c54ff).into()),
|
||||
|
@ -630,6 +632,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x50494599).into()),
|
||||
scrollbar_track_background: Some(rgba(0x32302fff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x32302f00).into()),
|
||||
editor_foreground: Some(rgba(0xebdbb2ff).into()),
|
||||
editor_background: Some(rgba(0x32302fff).into()),
|
||||
editor_gutter_background: Some(rgba(0x32302fff).into()),
|
||||
editor_line_number: Some(rgba(0x665c54ff).into()),
|
||||
|
@ -922,6 +925,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
|
||||
scrollbar_track_background: Some(rgba(0xf9f5d7ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xf9f5d700).into()),
|
||||
editor_foreground: Some(rgba(0x3c3836ff).into()),
|
||||
editor_background: Some(rgba(0xf9f5d7ff).into()),
|
||||
editor_gutter_background: Some(rgba(0xf9f5d7ff).into()),
|
||||
editor_line_number: Some(rgba(0xbdae93ff).into()),
|
||||
|
@ -1214,6 +1218,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
|
||||
scrollbar_track_background: Some(rgba(0xfbf1c7ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xfbf1c700).into()),
|
||||
editor_foreground: Some(rgba(0x3c3836ff).into()),
|
||||
editor_background: Some(rgba(0xfbf1c7ff).into()),
|
||||
editor_gutter_background: Some(rgba(0xfbf1c7ff).into()),
|
||||
editor_line_number: Some(rgba(0xbdae93ff).into()),
|
||||
|
@ -1506,6 +1511,7 @@ pub fn gruvbox() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
|
||||
scrollbar_track_background: Some(rgba(0xf2e5bcff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xf2e5bc00).into()),
|
||||
editor_foreground: Some(rgba(0x3c3836ff).into()),
|
||||
editor_background: Some(rgba(0xf2e5bcff).into()),
|
||||
editor_gutter_background: Some(rgba(0xf2e5bcff).into()),
|
||||
editor_line_number: Some(rgba(0xbdae93ff).into()),
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn night_owl() -> UserThemeFamily {
|
|||
scrollbar_thumb_hover_background: Some(rgba(0x084d8180).into()),
|
||||
scrollbar_thumb_border: Some(rgba(0x084d8180).into()),
|
||||
scrollbar_track_background: Some(rgba(0x011627ff).into()),
|
||||
editor_foreground: Some(rgba(0xd6deebff).into()),
|
||||
editor_background: Some(rgba(0x011627ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x011627ff).into()),
|
||||
editor_line_number: Some(rgba(0x4b6479ff).into()),
|
||||
|
@ -305,6 +306,7 @@ pub fn night_owl() -> UserThemeFamily {
|
|||
tab_inactive_background: Some(rgba(0xf0f0f0ff).into()),
|
||||
tab_active_background: Some(rgba(0xf6f6f6ff).into()),
|
||||
scrollbar_track_background: Some(rgba(0xfbfbfbff).into()),
|
||||
editor_foreground: Some(rgba(0x403f53ff).into()),
|
||||
editor_background: Some(rgba(0xfbfbfbff).into()),
|
||||
editor_gutter_background: Some(rgba(0xfbfbfbff).into()),
|
||||
editor_line_number: Some(rgba(0x90a7b2ff).into()),
|
||||
|
|
|
@ -47,6 +47,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x008ee633).into()),
|
||||
scrollbar_track_background: Some(rgba(0x07273bff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x07273bff).into()),
|
||||
editor_foreground: Some(rgba(0xbecfdaff).into()),
|
||||
editor_background: Some(rgba(0x07273bff).into()),
|
||||
editor_gutter_background: Some(rgba(0x07273bff).into()),
|
||||
editor_line_number: Some(rgba(0x4d6c80ff).into()),
|
||||
|
@ -324,6 +325,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0xeb609133).into()),
|
||||
scrollbar_track_background: Some(rgba(0x322a2dff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x322a2dff).into()),
|
||||
editor_foreground: Some(rgba(0xcbbec2ff).into()),
|
||||
editor_background: Some(rgba(0x322a2dff).into()),
|
||||
editor_gutter_background: Some(rgba(0x322a2dff).into()),
|
||||
editor_line_number: Some(rgba(0x715b63ff).into()),
|
||||
|
@ -601,6 +603,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0xf4f6f6ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xf4f6f6ff).into()),
|
||||
editor_foreground: Some(rgba(0x005661ff).into()),
|
||||
editor_background: Some(rgba(0xf4f6f6ff).into()),
|
||||
editor_gutter_background: Some(rgba(0xf4f6f6ff).into()),
|
||||
editor_line_number: Some(rgba(0xa0abacff).into()),
|
||||
|
@ -878,6 +881,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0xf2f1f8ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xf2f1f8ff).into()),
|
||||
editor_foreground: Some(rgba(0x0c006bff).into()),
|
||||
editor_background: Some(rgba(0xf2f1f8ff).into()),
|
||||
editor_gutter_background: Some(rgba(0xf2f1f8ff).into()),
|
||||
editor_line_number: Some(rgba(0x9d9ab1ff).into()),
|
||||
|
@ -1155,6 +1159,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0xfef8ecff).into()),
|
||||
scrollbar_track_border: Some(rgba(0xfef8ecff).into()),
|
||||
editor_foreground: Some(rgba(0x005661ff).into()),
|
||||
editor_background: Some(rgba(0xfef8ecff).into()),
|
||||
editor_gutter_background: Some(rgba(0xfef8ecff).into()),
|
||||
editor_line_number: Some(rgba(0xa0abacff).into()),
|
||||
|
@ -1432,6 +1437,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x3f7fa633).into()),
|
||||
scrollbar_track_background: Some(rgba(0x1b2932ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x1b2932ff).into()),
|
||||
editor_foreground: Some(rgba(0xc5cdd3ff).into()),
|
||||
editor_background: Some(rgba(0x1b2932ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x1b2932ff).into()),
|
||||
editor_line_number: Some(rgba(0x5d6e79ff).into()),
|
||||
|
@ -1709,6 +1715,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0x052529ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x052529ff).into()),
|
||||
editor_foreground: Some(rgba(0xb2cacdff).into()),
|
||||
editor_background: Some(rgba(0x052529ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x052529ff).into()),
|
||||
editor_line_number: Some(rgba(0x4e6b6eff).into()),
|
||||
|
@ -1986,6 +1993,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0x031417ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x031417ff).into()),
|
||||
editor_foreground: Some(rgba(0xb2cacdff).into()),
|
||||
editor_background: Some(rgba(0x031417ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x031417ff).into()),
|
||||
editor_line_number: Some(rgba(0x4e6b6eff).into()),
|
||||
|
@ -2263,6 +2271,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
|
||||
scrollbar_track_background: Some(rgba(0x031417ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x031417ff).into()),
|
||||
editor_foreground: Some(rgba(0xb2cacdff).into()),
|
||||
editor_background: Some(rgba(0x031417ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x031417ff).into()),
|
||||
editor_line_number: Some(rgba(0x4e6b6eff).into()),
|
||||
|
@ -2540,6 +2549,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x7060eb33).into()),
|
||||
scrollbar_track_background: Some(rgba(0x292640ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x292640ff).into()),
|
||||
editor_foreground: Some(rgba(0xc5c2d6ff).into()),
|
||||
editor_background: Some(rgba(0x292640ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x292640ff).into()),
|
||||
editor_line_number: Some(rgba(0x5c5973ff).into()),
|
||||
|
@ -2817,6 +2827,7 @@ pub fn noctis() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0xa660eb33).into()),
|
||||
scrollbar_track_background: Some(rgba(0x30243dff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x30243dff).into()),
|
||||
editor_foreground: Some(rgba(0xccbfd9ff).into()),
|
||||
editor_background: Some(rgba(0x30243dff).into()),
|
||||
editor_gutter_background: Some(rgba(0x30243dff).into()),
|
||||
editor_line_number: Some(rgba(0x665973ff).into()),
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn nord() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x434c5e99).into()),
|
||||
scrollbar_track_background: Some(rgba(0x2e3440ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x3b4252ff).into()),
|
||||
editor_foreground: Some(rgba(0xd8dee9ff).into()),
|
||||
editor_background: Some(rgba(0x2e3440ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x2e3440ff).into()),
|
||||
editor_line_number: Some(rgba(0x4c566aff).into()),
|
||||
|
|
|
@ -46,6 +46,7 @@ pub fn palenight() -> UserThemeFamily {
|
|||
scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
|
||||
scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
|
||||
scrollbar_track_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_foreground: Some(rgba(0xbfc7d5ff).into()),
|
||||
editor_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_gutter_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_line_number: Some(rgba(0x4c5374ff).into()),
|
||||
|
@ -331,6 +332,7 @@ pub fn palenight() -> UserThemeFamily {
|
|||
scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
|
||||
scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
|
||||
scrollbar_track_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_foreground: Some(rgba(0xbfc7d5ff).into()),
|
||||
editor_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_gutter_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_line_number: Some(rgba(0x4c5374ff).into()),
|
||||
|
@ -616,6 +618,7 @@ pub fn palenight() -> UserThemeFamily {
|
|||
scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
|
||||
scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
|
||||
scrollbar_track_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_foreground: Some(rgba(0xbfc7d5ff).into()),
|
||||
editor_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_gutter_background: Some(rgba(0x292d3eff).into()),
|
||||
editor_line_number: Some(rgba(0x4c5374ff).into()),
|
||||
|
|
|
@ -47,6 +47,7 @@ pub fn rose_pine() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6e6a8633).into()),
|
||||
scrollbar_track_background: Some(rgba(0x191724ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x6e6a8666).into()),
|
||||
editor_foreground: Some(rgba(0xe0def4ff).into()),
|
||||
editor_background: Some(rgba(0x191724ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x191724ff).into()),
|
||||
editor_line_number: Some(rgba(0x908caaff).into()),
|
||||
|
@ -306,6 +307,7 @@ pub fn rose_pine() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x817c9c26).into()),
|
||||
scrollbar_track_background: Some(rgba(0x232136ff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x817c9c4d).into()),
|
||||
editor_foreground: Some(rgba(0xe0def4ff).into()),
|
||||
editor_background: Some(rgba(0x232136ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x232136ff).into()),
|
||||
editor_line_number: Some(rgba(0x908caaff).into()),
|
||||
|
@ -565,6 +567,7 @@ pub fn rose_pine() -> UserThemeFamily {
|
|||
scrollbar_thumb_border: Some(rgba(0x6e6a8614).into()),
|
||||
scrollbar_track_background: Some(rgba(0xfaf4edff).into()),
|
||||
scrollbar_track_border: Some(rgba(0x6e6a8626).into()),
|
||||
editor_foreground: Some(rgba(0x575279ff).into()),
|
||||
editor_background: Some(rgba(0xfaf4edff).into()),
|
||||
editor_gutter_background: Some(rgba(0xfaf4edff).into()),
|
||||
editor_line_number: Some(rgba(0x797593ff).into()),
|
||||
|
|
|
@ -42,6 +42,7 @@ pub fn solarized() -> UserThemeFamily {
|
|||
tab_inactive_background: Some(rgba(0x004052ff).into()),
|
||||
tab_active_background: Some(rgba(0x002b37ff).into()),
|
||||
scrollbar_track_background: Some(rgba(0x002b36ff).into()),
|
||||
editor_foreground: Some(rgba(0xbbbbbbff).into()),
|
||||
editor_background: Some(rgba(0x002b36ff).into()),
|
||||
editor_gutter_background: Some(rgba(0x002b36ff).into()),
|
||||
editor_line_number: Some(rgba(0x566c74ff).into()),
|
||||
|
@ -307,6 +308,7 @@ pub fn solarized() -> UserThemeFamily {
|
|||
tab_inactive_background: Some(rgba(0xd3cbb7ff).into()),
|
||||
tab_active_background: Some(rgba(0xfdf6e3ff).into()),
|
||||
scrollbar_track_background: Some(rgba(0xfdf6e3ff).into()),
|
||||
editor_foreground: Some(rgba(0x333333ff).into()),
|
||||
editor_background: Some(rgba(0xfdf6e3ff).into()),
|
||||
editor_gutter_background: Some(rgba(0xfdf6e3ff).into()),
|
||||
editor_line_number: Some(rgba(0x9ca8a6ff).into()),
|
||||
|
|
|
@ -201,6 +201,7 @@ impl<'a> Debug for ThemeColorsRefinementPrinter<'a> {
|
|||
self.0.scrollbar_track_background,
|
||||
),
|
||||
("scrollbar_track_border", self.0.scrollbar_track_border),
|
||||
("editor_foreground", self.0.editor_foreground),
|
||||
("editor_background", self.0.editor_background),
|
||||
("editor_gutter_background", self.0.editor_gutter_background),
|
||||
(
|
||||
|
|
|
@ -132,6 +132,11 @@ impl VsCodeThemeConverter {
|
|||
.as_ref()
|
||||
.traverse(|color| try_parse_color(&color))?;
|
||||
|
||||
let vscode_editor_foreground = vscode_colors
|
||||
.editor_foreground
|
||||
.as_ref()
|
||||
.traverse(|color| try_parse_color(&color))?;
|
||||
|
||||
let vscode_editor_background = vscode_colors
|
||||
.editor_background
|
||||
.as_ref()
|
||||
|
@ -142,6 +147,16 @@ impl VsCodeThemeConverter {
|
|||
.as_ref()
|
||||
.traverse(|color| try_parse_color(&color))?;
|
||||
|
||||
let vscode_token_colors_foreground = self
|
||||
.theme
|
||||
.token_colors
|
||||
.iter()
|
||||
.find(|token_color| token_color.scope.is_none())
|
||||
.and_then(|token_color| token_color.settings.foreground.as_ref())
|
||||
.traverse(|color| try_parse_color(&color))
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
Ok(ThemeColorsRefinement {
|
||||
border: vscode_panel_border,
|
||||
border_variant: vscode_panel_border,
|
||||
|
@ -197,16 +212,7 @@ impl VsCodeThemeConverter {
|
|||
.foreground
|
||||
.as_ref()
|
||||
.traverse(|color| try_parse_color(&color))?
|
||||
.or_else(|| {
|
||||
self.theme
|
||||
.token_colors
|
||||
.iter()
|
||||
.find(|token_color| token_color.scope.is_none())
|
||||
.and_then(|token_color| token_color.settings.foreground.as_ref())
|
||||
.traverse(|color| try_parse_color(&color))
|
||||
.ok()
|
||||
.flatten()
|
||||
}),
|
||||
.or(vscode_token_colors_foreground),
|
||||
text_muted: vscode_colors
|
||||
.tab_inactive_foreground
|
||||
.as_ref()
|
||||
|
@ -226,6 +232,7 @@ impl VsCodeThemeConverter {
|
|||
.as_ref()
|
||||
.traverse(|color| try_parse_color(&color))?
|
||||
.or(vscode_editor_background),
|
||||
editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground),
|
||||
editor_background: vscode_editor_background,
|
||||
editor_gutter_background: vscode_editor_background,
|
||||
editor_line_number: vscode_colors
|
||||
|
|
|
@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
|
|||
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::h_stack;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait ButtonCommon: Clickable + Disableable {
|
||||
|
@ -250,6 +249,7 @@ impl ButtonSize {
|
|||
/// This is also used to build the prebuilt buttons.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonLike {
|
||||
base: Div,
|
||||
id: ElementId,
|
||||
pub(super) style: ButtonStyle,
|
||||
pub(super) disabled: bool,
|
||||
|
@ -264,6 +264,7 @@ pub struct ButtonLike {
|
|||
impl ButtonLike {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
style: ButtonStyle::default(),
|
||||
disabled: false,
|
||||
|
@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
|
|||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for ButtonLike {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
|
@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
|
|||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack()
|
||||
self.base
|
||||
.h_flex()
|
||||
.id(self.id.clone())
|
||||
.group("")
|
||||
.flex_none()
|
||||
|
|
|
@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
|
|||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for IconButton {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
type Rendered = ButtonLike;
|
||||
|
||||
|
|
|
@ -255,6 +255,9 @@ impl Render for ContextMenu {
|
|||
};
|
||||
|
||||
ListItem::new(label.clone())
|
||||
.inset(true)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| handler(cx))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
|
@ -265,8 +268,6 @@ impl Render for ContextMenu {
|
|||
.map(|binding| div().ml_1().child(binding))
|
||||
})),
|
||||
)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| handler(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||
use gpui::ClickEvent;
|
||||
|
||||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disclosure {
|
||||
is_open: bool,
|
||||
|
|
|
@ -50,6 +50,7 @@ pub enum Icon {
|
|||
CopilotError,
|
||||
CopilotDisabled,
|
||||
Dash,
|
||||
Disconnected,
|
||||
Envelope,
|
||||
ExternalLink,
|
||||
ExclamationTriangle,
|
||||
|
@ -129,6 +130,7 @@ impl Icon {
|
|||
Icon::CopilotError => "icons/copilot_error.svg",
|
||||
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
|
||||
Icon::Dash => "icons/dash.svg",
|
||||
Icon::Disconnected => "icons/disconnected.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||
Icon::ExternalLink => "icons/external_link.svg",
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label};
|
||||
use crate::{h_stack, prelude::*, Disclosure, Label};
|
||||
use gpui::{AnyElement, ClickEvent, Div};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
/// The label of the header.
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
meta: SmallVec<[AnyElement; 2]>,
|
||||
/// A slot for content that appears before the label, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the label
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
inset: bool,
|
||||
|
@ -17,8 +23,9 @@ impl ListHeader {
|
|||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
meta: SmallVec::new(),
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
inset: false,
|
||||
toggle: None,
|
||||
on_toggle: None,
|
||||
|
@ -39,13 +46,23 @@ impl ListHeader {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.left_icon = left_icon.into();
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: impl IntoElement) -> Self {
|
||||
self.meta.push(meta.into_any_element());
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset(mut self, inset: bool) -> Self {
|
||||
self.inset = inset;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
|
|||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack().w_full().relative().child(
|
||||
h_stack().w_full().relative().group("list_header").child(
|
||||
div()
|
||||
.h_5()
|
||||
.h_7()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
|
@ -77,24 +94,29 @@ impl RenderOnce for ListHeader {
|
|||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.children(self.start_slot)
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
)
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
),
|
||||
)
|
||||
.child(h_stack().gap_2().items_center().children(self.meta)),
|
||||
.child(h_stack().children(self.end_slot))
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_0()
|
||||
.visible_on_hover("list_header")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
|
||||
use gpui::{
|
||||
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
|
||||
Stateful,
|
||||
px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{prelude::*, Disclosure};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
selected: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: usize,
|
||||
indent_step_size: Pixels,
|
||||
left_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears before the children, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the children, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the children
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
inset: bool,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
|
@ -30,7 +35,9 @@ impl ListItem {
|
|||
selected: false,
|
||||
indent_level: 0,
|
||||
indent_step_size: px(12.),
|
||||
left_slot: None,
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
toggle: None,
|
||||
inset: false,
|
||||
on_click: None,
|
||||
|
@ -87,23 +94,18 @@ impl ListItem {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
|
||||
self.left_slot = Some(left_content.into_any_element());
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
||||
self.left_slot = Some(
|
||||
IconElement::new(left_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
);
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
||||
self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -125,49 +127,102 @@ impl RenderOnce for ListItem {
|
|||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
h_stack()
|
||||
.id(self.id)
|
||||
.w_full()
|
||||
.relative()
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.when(self.inset, |this| this.rounded_md())
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
// When an item is inset draw the indent spacing outside of the item
|
||||
.when(self.inset, |this| {
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.px_1()
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(move |event, cx| {
|
||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||
// but we only care about the left button.
|
||||
if event.down.button == MouseButton::Left {
|
||||
(on_click)(event, cx)
|
||||
}
|
||||
})
|
||||
.when(!self.inset, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
.child(
|
||||
div()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
h_stack()
|
||||
.id("inner_list_item")
|
||||
.w_full()
|
||||
.relative()
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
.gap_1()
|
||||
.px_2()
|
||||
.group("list_item")
|
||||
.when(self.inset, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(on_click)
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
.map(|this| {
|
||||
if self.inset {
|
||||
this.rounded_md()
|
||||
} else {
|
||||
// When an item is not inset draw the indent spacing inside of the item
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
}
|
||||
})
|
||||
.children(self.toggle.map(|is_open| {
|
||||
div()
|
||||
.flex()
|
||||
.absolute()
|
||||
.left(rems(-1.))
|
||||
.visible_on_hover("")
|
||||
.child(Disclosure::new(is_open).on_toggle(self.on_toggle))
|
||||
}))
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.children(self.start_slot)
|
||||
.children(self.children),
|
||||
)
|
||||
.children(self.left_slot)
|
||||
.children(self.children),
|
||||
.when_some(self.end_slot, |this, end_slot| {
|
||||
this.justify_between().child(
|
||||
h_stack()
|
||||
.when(self.end_hover_slot.is_some(), |this| {
|
||||
this.visible()
|
||||
.group_hover("list_item", |this| this.invisible())
|
||||
})
|
||||
.child(end_slot),
|
||||
)
|
||||
})
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_2()
|
||||
.top_0()
|
||||
.visible_on_hover("list_item")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
|
|||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.my_1()
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
|||
#[derive(IntoElement)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
start_slot: Option<Icon>,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
|
@ -14,13 +14,13 @@ impl ListSubHeader {
|
|||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
start_slot: None,
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self.start_slot = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
|
|||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
.children(self.start_slot.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
|
|
|
@ -182,12 +182,12 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
_: Bounds<gpui::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(child) = element_state.child_element.take() {
|
||||
if let Some(mut child) = element_state.child_element.take() {
|
||||
child.paint(cx);
|
||||
}
|
||||
|
||||
|
@ -195,7 +195,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
|
|||
element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
|
||||
}
|
||||
|
||||
if let Some(menu) = element_state.menu_element.take() {
|
||||
if let Some(mut menu) = element_state.menu_element.take() {
|
||||
menu.paint(cx);
|
||||
|
||||
if let Some(child_bounds) = element_state.child_bounds {
|
||||
|
|
|
@ -112,21 +112,21 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: Bounds<gpui::Pixels>,
|
||||
element_state: &mut Self::State,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(child) = element_state.child_element.take() {
|
||||
if let Some(mut child) = element_state.child_element.take() {
|
||||
child.paint(cx);
|
||||
}
|
||||
|
||||
if let Some(menu) = element_state.menu_element.take() {
|
||||
if let Some(mut menu) = element_state.menu_element.take() {
|
||||
menu.paint(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(builder) = self.menu_builder else {
|
||||
let Some(builder) = self.menu_builder.take() else {
|
||||
return;
|
||||
};
|
||||
let menu = element_state.menu.clone();
|
||||
|
|
|
@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
|
|||
.child(Story::label("Default"))
|
||||
.child(ListHeader::new("Section 1"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
|
||||
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
|
||||
.child(Story::label("With left icon and meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 3")
|
||||
.left_icon(Icon::BellOff)
|
||||
.meta(IconButton::new("action_1", Icon::Bolt)),
|
||||
.start_slot(IconElement::new(Icon::BellOff))
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt)),
|
||||
)
|
||||
.child(Story::label("With multiple meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 4")
|
||||
.meta(IconButton::new("action_1", Icon::Bolt))
|
||||
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||
.meta(IconButton::new("action_3", Icon::Plus)),
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt))
|
||||
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||
.end_slot(IconButton::new("action_3", Icon::Plus)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, Avatar};
|
||||
use crate::{Icon, ListItem};
|
||||
|
||||
pub struct ListItemStory;
|
||||
|
@ -9,24 +9,80 @@ pub struct ListItemStory;
|
|||
impl Render for ListItemStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(Story::label("Inset"))
|
||||
.child(
|
||||
ListItem::new("with_left_icon")
|
||||
ListItem::new("inset_list_item")
|
||||
.inset(true)
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child("Hello, world!")
|
||||
.left_icon(Icon::Bell),
|
||||
.end_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With left avatar"))
|
||||
.child(Story::label("With start slot icon"))
|
||||
.child(
|
||||
ListItem::new("with start slot_icon")
|
||||
.child("Hello, world!")
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With start slot avatar"))
|
||||
.child(
|
||||
ListItem::new("with_start slot avatar")
|
||||
.child("Hello, world!")
|
||||
.start_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end slot"))
|
||||
.child(
|
||||
ListItem::new("with_left_avatar")
|
||||
.child("Hello, world!")
|
||||
.left_avatar(SharedString::from(
|
||||
.end_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
)),
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end hover slot"))
|
||||
.child(
|
||||
ListItem::new("with_end_hover_slot")
|
||||
.child("Hello, world!")
|
||||
.end_slot(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
))),
|
||||
)
|
||||
.end_hover_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
|
|
|
@ -158,7 +158,6 @@ impl RenderOnce for Tab {
|
|||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.invisible()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
|
@ -167,7 +166,7 @@ impl RenderOnce for Tab {
|
|||
TabCloseSide::Start => this.left_1(),
|
||||
TabCloseSide::End => this.right_1(),
|
||||
})
|
||||
.group_hover("", |style| style.visible())
|
||||
.visible_on_hover("")
|
||||
.children(self.end_slot),
|
||||
)
|
||||
.children(self.children),
|
||||
|
|
|
@ -9,6 +9,7 @@ pub use crate::clickable::*;
|
|||
pub use crate::disableable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::visible_on_hover::*;
|
||||
pub use crate::{h_stack, v_stack};
|
||||
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
|
||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||
|
|
|
@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
|
|||
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||
}
|
||||
|
||||
/// The theme's primary border color.
|
||||
fn border_primary(self, cx: &mut WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border)
|
||||
}
|
||||
|
||||
/// The theme's secondary or muted border color.
|
||||
fn border_muted(self, cx: &mut WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border_variant)
|
||||
}
|
||||
|
||||
fn debug_bg_red(self) -> Self {
|
||||
self.bg(gpui::red())
|
||||
self.bg(hsla(0. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_green(self) -> Self {
|
||||
self.bg(gpui::green())
|
||||
self.bg(hsla(120. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_blue(self) -> Self {
|
||||
self.bg(gpui::blue())
|
||||
self.bg(hsla(240. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_yellow(self) -> Self {
|
||||
|
|
|
@ -20,14 +20,14 @@ pub enum ElevationIndex {
|
|||
}
|
||||
|
||||
impl ElevationIndex {
|
||||
pub fn z_index(self) -> u32 {
|
||||
pub fn z_index(self) -> u8 {
|
||||
match self {
|
||||
ElevationIndex::Background => 0,
|
||||
ElevationIndex::Surface => 100,
|
||||
ElevationIndex::ElevatedSurface => 200,
|
||||
ElevationIndex::Wash => 250,
|
||||
ElevationIndex::ModalSurface => 300,
|
||||
ElevationIndex::DraggedElement => 900,
|
||||
ElevationIndex::Surface => 42,
|
||||
ElevationIndex::ElevatedSurface => 84,
|
||||
ElevationIndex::Wash => 126,
|
||||
ElevationIndex::ModalSurface => 168,
|
||||
ElevationIndex::DraggedElement => 210,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ mod selectable;
|
|||
mod styled_ext;
|
||||
mod styles;
|
||||
pub mod utils;
|
||||
mod visible_on_hover;
|
||||
|
||||
pub use clickable::*;
|
||||
pub use components::*;
|
||||
|
@ -30,3 +31,4 @@ pub use prelude::*;
|
|||
pub use selectable::*;
|
||||
pub use styled_ext::*;
|
||||
pub use styles::*;
|
||||
pub use visible_on_hover::*;
|
||||
|
|
15
crates/ui2/src/visible_on_hover.rs
Normal file
15
crates/ui2/src/visible_on_hover.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use gpui::{InteractiveElement, SharedString, Styled};
|
||||
|
||||
pub trait VisibleOnHover {
|
||||
/// Sets the element to only be visible when the specified group is hovered.
|
||||
///
|
||||
/// Pass `""` as the `group_name` to use the global group.
|
||||
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement + Styled> VisibleOnHover for E {
|
||||
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.invisible()
|
||||
.group_hover(group_name, |style| style.visible())
|
||||
}
|
||||
}
|
17
crates/vcs_menu2/Cargo.toml
Normal file
17
crates/vcs_menu2/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "vcs_menu2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
|
||||
fs = {package = "fs2", path = "../fs2"}
|
||||
gpui = {package = "gpui2", path = "../gpui2"}
|
||||
picker = {package = "picker2", path = "../picker2"}
|
||||
util = {path = "../util"}
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = {package = "workspace2", path = "../workspace2"}
|
||||
|
||||
anyhow.workspace = true
|
359
crates/vcs_menu2/src/lib.rs
Normal file
359
crates/vcs_menu2/src/lib.rs
Normal file
|
@ -0,0 +1,359 @@
|
|||
use anyhow::{anyhow, bail, Result};
|
||||
use fs::repository::Branch;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Toast, Workspace};
|
||||
|
||||
actions!(branches, [OpenRecent]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
// todo!() po
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, action, cx| {
|
||||
ModalBranchList::toggle_modal(workspace, action, cx).log_err();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
pub type BranchList = Picker<BranchListDelegate>;
|
||||
|
||||
pub struct ModalBranchList {
|
||||
pub picker: View<Picker<BranchListDelegate>>,
|
||||
}
|
||||
|
||||
impl ModalView for ModalBranchList {}
|
||||
impl EventEmitter<DismissEvent> for ModalBranchList {}
|
||||
|
||||
impl FocusableView for ModalBranchList {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModalBranchList {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_branch_list(
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Result<View<BranchList>> {
|
||||
let delegate = workspace.update(cx, |workspace, cx| {
|
||||
BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
|
||||
})?;
|
||||
|
||||
Ok(cx.build_view(|cx| Picker::new(delegate, cx)))
|
||||
}
|
||||
|
||||
impl ModalBranchList {
|
||||
fn toggle_modal(
|
||||
workspace: &mut Workspace,
|
||||
_: &OpenRecent,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Result<()> {
|
||||
// Modal branch picker has a longer trailoff than a popover one.
|
||||
let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
|
||||
workspace.toggle_modal(cx, |cx| ModalBranchList {
|
||||
picker: cx.build_view(|cx| Picker::new(delegate, cx)),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<StringMatch>,
|
||||
all_branches: Vec<Branch>,
|
||||
workspace: View<Workspace>,
|
||||
selected_index: usize,
|
||||
last_query: String,
|
||||
/// Max length of branch name before we truncate it and add a trailing `...`.
|
||||
branch_name_trailoff_after: usize,
|
||||
}
|
||||
|
||||
impl BranchListDelegate {
|
||||
fn new(
|
||||
workspace: &Workspace,
|
||||
handle: View<Workspace>,
|
||||
branch_name_trailoff_after: usize,
|
||||
cx: &AppContext,
|
||||
) -> Result<Self> {
|
||||
let project = workspace.project().read(&cx);
|
||||
let Some(worktree) = project.visible_worktrees(cx).next() else {
|
||||
bail!("Cannot update branch list as there are no visible worktrees")
|
||||
};
|
||||
|
||||
let mut cwd = worktree.read(cx).abs_path().to_path_buf();
|
||||
cwd.push(".git");
|
||||
let Some(repo) = project.fs().open_repo(&cwd) else {
|
||||
bail!("Project does not have associated git repository.")
|
||||
};
|
||||
let all_branches = repo.lock().branches()?;
|
||||
Ok(Self {
|
||||
matches: vec![],
|
||||
workspace: handle,
|
||||
all_branches,
|
||||
selected_index: 0,
|
||||
last_query: Default::default(),
|
||||
branch_name_trailoff_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
|
||||
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
|
||||
self.workspace.update(cx, |model, ctx| {
|
||||
model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for BranchListDelegate {
|
||||
type ListItem = ListItem;
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Select branch...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let candidates = picker.update(&mut cx, |view, _| {
|
||||
const RECENT_BRANCHES_COUNT: usize = 10;
|
||||
let mut branches = view.delegate.all_branches.clone();
|
||||
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
|
||||
// Truncate list of recent branches
|
||||
// Do a partial sort to show recent-ish branches first.
|
||||
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
|
||||
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
|
||||
});
|
||||
branches.truncate(RECENT_BRANCHES_COUNT);
|
||||
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
|
||||
}
|
||||
branches
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
char_bag: command.name.chars().collect(),
|
||||
string: command.name.into(),
|
||||
})
|
||||
.collect::<Vec<StringMatchCandidate>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
return;
|
||||
};
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
|
||||
}
|
||||
delegate.last_query = query;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let current_pick = self.selected_index();
|
||||
let Some(current_pick) = self
|
||||
.matches
|
||||
.get(current_pick)
|
||||
.map(|pick| pick.string.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |this, cx| {
|
||||
let project = this.delegate.workspace.read(cx).project().read(cx);
|
||||
let mut cwd = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
|
||||
.read(cx)
|
||||
.abs_path()
|
||||
.to_path_buf();
|
||||
cwd.push(".git");
|
||||
let status = project
|
||||
.fs()
|
||||
.open_repo(&cwd)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Could not open repository at path `{}`",
|
||||
cwd.as_os_str().to_string_lossy()
|
||||
)
|
||||
})?
|
||||
.lock()
|
||||
.change_branch(¤t_pick);
|
||||
if status.is_err() {
|
||||
this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
|
||||
status?;
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = &self.matches[ix];
|
||||
let shortened_branch_name =
|
||||
util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
|
||||
let highlights: Vec<_> = hit
|
||||
.positions
|
||||
.iter()
|
||||
.filter(|index| index < &&self.branch_name_trailoff_after)
|
||||
.copied()
|
||||
.collect();
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
|
||||
.start_slot(HighlightedLabel::new(shortened_branch_name, highlights))
|
||||
.selected(selected),
|
||||
)
|
||||
}
|
||||
// fn render_header(
|
||||
// &self,
|
||||
// cx: &mut ViewContext<Picker<Self>>,
|
||||
// ) -> Option<AnyElement<Picker<Self>>> {
|
||||
// let theme = &theme::current(cx);
|
||||
// let style = theme.picker.header.clone();
|
||||
// let label = if self.last_query.is_empty() {
|
||||
// Flex::row()
|
||||
// .with_child(Label::new("Recent branches", style.label.clone()))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// } else {
|
||||
// Flex::row()
|
||||
// .with_child(Label::new("Branches", style.label.clone()))
|
||||
// .with_children(self.matches.is_empty().not().then(|| {
|
||||
// let suffix = if self.matches.len() == 1 { "" } else { "es" };
|
||||
// Label::new(
|
||||
// format!("{} match{}", self.matches.len(), suffix),
|
||||
// style.label,
|
||||
// )
|
||||
// .flex_float()
|
||||
// }))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// };
|
||||
// Some(label.into_any())
|
||||
// }
|
||||
// fn render_footer(
|
||||
// &self,
|
||||
// cx: &mut ViewContext<Picker<Self>>,
|
||||
// ) -> Option<AnyElement<Picker<Self>>> {
|
||||
// if !self.last_query.is_empty() {
|
||||
// let theme = &theme::current(cx);
|
||||
// let style = theme.picker.footer.clone();
|
||||
// enum BranchCreateButton {}
|
||||
// Some(
|
||||
// Flex::row().with_child(MouseEventHandler::new::<BranchCreateButton, _>(0, cx, |state, _| {
|
||||
// let style = style.style_for(state);
|
||||
// Label::new("Create branch", style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_down(MouseButton::Left, |_, _, cx| {
|
||||
// cx.spawn(|picker, mut cx| async move {
|
||||
// picker.update(&mut cx, |this, cx| {
|
||||
// let project = this.delegate().workspace.read(cx).project().read(cx);
|
||||
// let current_pick = &this.delegate().last_query;
|
||||
// let mut cwd = project
|
||||
// .visible_worktrees(cx)
|
||||
// .next()
|
||||
// .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
|
||||
// .read(cx)
|
||||
// .abs_path()
|
||||
// .to_path_buf();
|
||||
// cwd.push(".git");
|
||||
// let repo = project
|
||||
// .fs()
|
||||
// .open_repo(&cwd)
|
||||
// .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
|
||||
// let repo = repo
|
||||
// .lock();
|
||||
// let status = repo
|
||||
// .create_branch(¤t_pick);
|
||||
// if status.is_err() {
|
||||
// this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
|
||||
// status?;
|
||||
// }
|
||||
// let status = repo.change_branch(¤t_pick);
|
||||
// if status.is_err() {
|
||||
// this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
|
||||
// status?;
|
||||
// }
|
||||
// cx.emit(PickerEvent::Dismiss);
|
||||
// Ok::<(), anyhow::Error>(())
|
||||
// })
|
||||
// }).detach();
|
||||
// })).aligned().right()
|
||||
// .into_any(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::DraggedDock;
|
||||
use crate::{status_bar::StatusItemView, Workspace};
|
||||
use crate::{DockClickReset, DockDragState};
|
||||
use gpui::{
|
||||
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
|
||||
|
@ -41,7 +41,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
|||
}
|
||||
|
||||
pub trait PanelHandle: Send + Sync {
|
||||
fn entity_id(&self) -> EntityId;
|
||||
fn panel_id(&self) -> EntityId;
|
||||
fn persistent_name(&self) -> &'static str;
|
||||
fn position(&self, cx: &WindowContext) -> DockPosition;
|
||||
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
|
||||
|
@ -62,7 +62,7 @@ impl<T> PanelHandle for View<T>
|
|||
where
|
||||
T: Panel,
|
||||
{
|
||||
fn entity_id(&self) -> EntityId {
|
||||
fn panel_id(&self) -> EntityId {
|
||||
Entity::entity_id(self)
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ pub struct Dock {
|
|||
is_open: bool,
|
||||
active_panel_index: usize,
|
||||
focus_handle: FocusHandle,
|
||||
focus_subscription: Subscription,
|
||||
_focus_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl FocusableView for Dock {
|
||||
|
@ -187,7 +187,6 @@ struct PanelEntry {
|
|||
|
||||
pub struct PanelButtons {
|
||||
dock: View<Dock>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Dock {
|
||||
|
@ -204,7 +203,7 @@ impl Dock {
|
|||
active_panel_index: 0,
|
||||
is_open: false,
|
||||
focus_handle,
|
||||
focus_subscription,
|
||||
_focus_subscription: focus_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +260,7 @@ impl Dock {
|
|||
|
||||
pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
|
||||
for entry in &mut self.panel_entries {
|
||||
if entry.panel.entity_id() == panel.entity_id() {
|
||||
if entry.panel.panel_id() == panel.entity_id() {
|
||||
if zoomed != entry.panel.is_zoomed(cx) {
|
||||
entry.panel.set_zoomed(zoomed, cx);
|
||||
}
|
||||
|
@ -309,7 +308,7 @@ impl Dock {
|
|||
|
||||
let was_visible = this.is_open()
|
||||
&& this.visible_panel().map_or(false, |active_panel| {
|
||||
active_panel.entity_id() == Entity::entity_id(&panel)
|
||||
active_panel.panel_id() == Entity::entity_id(&panel)
|
||||
});
|
||||
|
||||
this.remove_panel(&panel, cx);
|
||||
|
@ -351,7 +350,7 @@ impl Dock {
|
|||
if let Some(ix) = this
|
||||
.panel_entries
|
||||
.iter()
|
||||
.position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
|
||||
.position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
|
||||
{
|
||||
this.set_open(true, cx);
|
||||
this.activate_panel(ix, cx);
|
||||
|
@ -361,7 +360,7 @@ impl Dock {
|
|||
PanelEvent::Close => {
|
||||
if this
|
||||
.visible_panel()
|
||||
.map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
|
||||
.map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
|
||||
{
|
||||
this.set_open(false, cx);
|
||||
}
|
||||
|
@ -389,7 +388,7 @@ impl Dock {
|
|||
if let Some(panel_ix) = self
|
||||
.panel_entries
|
||||
.iter()
|
||||
.position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
|
||||
.position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
|
||||
{
|
||||
if panel_ix == self.active_panel_index {
|
||||
self.active_panel_index = 0;
|
||||
|
@ -450,7 +449,7 @@ impl Dock {
|
|||
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
|
||||
self.panel_entries
|
||||
.iter()
|
||||
.find(|entry| entry.panel.entity_id() == panel.entity_id())
|
||||
.find(|entry| entry.panel.panel_id() == panel.panel_id())
|
||||
.map(|entry| entry.panel.size(cx))
|
||||
}
|
||||
|
||||
|
@ -487,62 +486,72 @@ impl Render for Dock {
|
|||
if let Some(entry) = self.visible_entry() {
|
||||
let size = entry.panel.size(cx);
|
||||
|
||||
let mut pre_resize_handle = None;
|
||||
let mut post_resize_handle = None;
|
||||
let position = self.position;
|
||||
let handler = div()
|
||||
let mut handle = div()
|
||||
.id("resize-handle")
|
||||
.bg(cx.theme().colors().border)
|
||||
.on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
|
||||
cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
|
||||
.on_drag(DraggedDock(position), |dock, cx| {
|
||||
cx.build_view(|_| dock.clone())
|
||||
})
|
||||
.on_click(cx.listener(|v, e: &ClickEvent, cx| {
|
||||
if e.down.button == MouseButton::Left {
|
||||
cx.update_global(|state: &mut DockClickReset, cx| {
|
||||
if state.0.is_some() {
|
||||
state.0 = None;
|
||||
v.resize_active_panel(None, cx)
|
||||
} else {
|
||||
let double_click = cx.double_click_interval();
|
||||
let timer = cx.background_executor().timer(double_click);
|
||||
state.0 = Some(cx.spawn(|_, mut cx| async move {
|
||||
timer.await;
|
||||
cx.update_global(|state: &mut DockClickReset, cx| {
|
||||
state.0 = None;
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
})
|
||||
if e.down.button == MouseButton::Left && e.down.click_count == 2 {
|
||||
v.resize_active_panel(None, cx)
|
||||
}
|
||||
}));
|
||||
}))
|
||||
.z_index(1);
|
||||
|
||||
const HANDLE_SIZE: Pixels = Pixels(6.);
|
||||
|
||||
match self.position() {
|
||||
DockPosition::Left => {
|
||||
post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
|
||||
handle = handle
|
||||
.absolute()
|
||||
.right(px(0.))
|
||||
.top(px(0.))
|
||||
.h_full()
|
||||
.w(HANDLE_SIZE)
|
||||
.cursor_col_resize();
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
|
||||
handle = handle
|
||||
.absolute()
|
||||
.top(px(0.))
|
||||
.left(px(0.))
|
||||
.w_full()
|
||||
.h(HANDLE_SIZE)
|
||||
.cursor_row_resize();
|
||||
}
|
||||
DockPosition::Right => {
|
||||
pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
|
||||
handle = handle
|
||||
.absolute()
|
||||
.top(px(0.))
|
||||
.left(px(0.))
|
||||
.w_full()
|
||||
.h(HANDLE_SIZE)
|
||||
.cursor_col_resize();
|
||||
}
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.map(|this| match self.position().axis() {
|
||||
Axis::Horizontal => this.w(px(size)).h_full(),
|
||||
Axis::Vertical => this.h(px(size)).w_full(),
|
||||
Axis::Horizontal => this.w(px(size)).h_full().flex_row(),
|
||||
Axis::Vertical => this.h(px(size)).w_full().flex_col(),
|
||||
})
|
||||
.map(|this| match self.position() {
|
||||
DockPosition::Left => this.border_r(),
|
||||
DockPosition::Right => this.border_l(),
|
||||
DockPosition::Bottom => this.border_t(),
|
||||
})
|
||||
.children(pre_resize_handle)
|
||||
.child(entry.panel.to_any())
|
||||
.children(post_resize_handle)
|
||||
.child(
|
||||
div()
|
||||
.map(|this| match self.position().axis() {
|
||||
Axis::Horizontal => this.min_w(px(size)).h_full(),
|
||||
Axis::Vertical => this.min_h(px(size)).w_full(),
|
||||
})
|
||||
.child(entry.panel.to_any()),
|
||||
)
|
||||
.child(handle)
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
|
@ -550,166 +559,12 @@ impl Render for Dock {
|
|||
}
|
||||
|
||||
impl PanelButtons {
|
||||
pub fn new(
|
||||
dock: View<Dock>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
|
||||
cx.observe(&dock, |_, _, cx| cx.notify()).detach();
|
||||
Self { dock, workspace }
|
||||
Self { dock }
|
||||
}
|
||||
}
|
||||
|
||||
// impl Render for PanelButtons {
|
||||
// type Element = ();
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
// todo!("")
|
||||
// }
|
||||
|
||||
// fn ui_name() -> &'static str {
|
||||
// "PanelButtons"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// let theme = &settings::get::<ThemeSettings>(cx).theme;
|
||||
// let tooltip_style = theme.tooltip.clone();
|
||||
// let theme = &theme.workspace.status_bar.panel_buttons;
|
||||
// let button_style = theme.button.clone();
|
||||
// let dock = self.dock.read(cx);
|
||||
// let active_ix = dock.active_panel_index;
|
||||
// let is_open = dock.is_open;
|
||||
// let dock_position = dock.position;
|
||||
// let group_style = match dock_position {
|
||||
// DockPosition::Left => theme.group_left,
|
||||
// DockPosition::Bottom => theme.group_bottom,
|
||||
// DockPosition::Right => theme.group_right,
|
||||
// };
|
||||
// let menu_corner = match dock_position {
|
||||
// DockPosition::Left => AnchorCorner::BottomLeft,
|
||||
// DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
|
||||
// };
|
||||
|
||||
// let panels = dock
|
||||
// .panel_entries
|
||||
// .iter()
|
||||
// .map(|item| (item.panel.clone(), item.context_menu.clone()))
|
||||
// .collect::<Vec<_>>();
|
||||
// Flex::row()
|
||||
// .with_children(panels.into_iter().enumerate().filter_map(
|
||||
// |(panel_ix, (view, context_menu))| {
|
||||
// let icon_path = view.icon_path(cx)?;
|
||||
// let is_active = is_open && panel_ix == active_ix;
|
||||
// let (tooltip, tooltip_action) = if is_active {
|
||||
// (
|
||||
// format!("Close {} dock", dock_position.to_label()),
|
||||
// Some(match dock_position {
|
||||
// DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
|
||||
// DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
|
||||
// DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
|
||||
// }),
|
||||
// )
|
||||
// } else {
|
||||
// view.icon_tooltip(cx)
|
||||
// };
|
||||
// Some(
|
||||
// Stack::new()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
|
||||
// let style = button_style.in_state(is_active);
|
||||
|
||||
// let style = style.style_for(state);
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Svg::new(icon_path)
|
||||
// .with_color(style.icon_color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_size)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .with_children(if let Some(label) = view.icon_label(cx) {
|
||||
// Some(
|
||||
// Label::new(label, style.label.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.label.container)
|
||||
// .aligned(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// })
|
||||
// .constrained()
|
||||
// .with_height(style.icon_size)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, {
|
||||
// let tooltip_action =
|
||||
// tooltip_action.as_ref().map(|action| action.boxed_clone());
|
||||
// move |_, this, cx| {
|
||||
// if let Some(tooltip_action) = &tooltip_action {
|
||||
// let window = cx.window();
|
||||
// let view_id = this.workspace.id();
|
||||
// let tooltip_action = tooltip_action.boxed_clone();
|
||||
// cx.spawn(|_, mut cx| async move {
|
||||
// window.dispatch_action(
|
||||
// view_id,
|
||||
// &*tooltip_action,
|
||||
// &mut cx,
|
||||
// );
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .on_click(MouseButton::Right, {
|
||||
// let view = view.clone();
|
||||
// let menu = context_menu.clone();
|
||||
// move |_, _, cx| {
|
||||
// const POSITIONS: [DockPosition; 3] = [
|
||||
// DockPosition::Left,
|
||||
// DockPosition::Right,
|
||||
// DockPosition::Bottom,
|
||||
// ];
|
||||
|
||||
// menu.update(cx, |menu, cx| {
|
||||
// let items = POSITIONS
|
||||
// .into_iter()
|
||||
// .filter(|position| {
|
||||
// *position != dock_position
|
||||
// && view.position_is_valid(*position, cx)
|
||||
// })
|
||||
// .map(|position| {
|
||||
// let view = view.clone();
|
||||
// ContextMenuItem::handler(
|
||||
// format!("Dock {}", position.to_label()),
|
||||
// move |cx| view.set_position(position, cx),
|
||||
// )
|
||||
// })
|
||||
// .collect();
|
||||
// menu.show(Default::default(), menu_corner, items, cx);
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// .with_tooltip::<Self>(
|
||||
// panel_ix,
|
||||
// tooltip,
|
||||
// tooltip_action,
|
||||
// tooltip_style.clone(),
|
||||
// cx,
|
||||
// ),
|
||||
// )
|
||||
// .with_child(ChildView::new(&context_menu, cx)),
|
||||
// )
|
||||
// },
|
||||
// ))
|
||||
// .contained()
|
||||
// .with_style(group_style)
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// here be kittens
|
||||
impl Render for PanelButtons {
|
||||
type Element = Div;
|
||||
|
||||
|
|
|
@ -920,7 +920,7 @@ pub mod test {
|
|||
impl EventEmitter<ItemEvent> for TestItem {}
|
||||
|
||||
impl FocusableView for TestItem {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
@ -942,8 +942,8 @@ pub mod test {
|
|||
fn tab_content(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
selected: bool,
|
||||
cx: &ui::prelude::WindowContext,
|
||||
_selected: bool,
|
||||
_cx: &ui::prelude::WindowContext,
|
||||
) -> AnyElement {
|
||||
self.tab_detail.set(detail);
|
||||
gpui::div().into_any_element()
|
||||
|
|
|
@ -5,7 +5,7 @@ use gpui::{
|
|||
use ui::{h_stack, v_stack};
|
||||
|
||||
pub trait ModalView: ManagedView {
|
||||
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ impl<V: ModalView> ModalViewHandle for View<V> {
|
|||
|
||||
pub struct ActiveModal {
|
||||
modal: Box<dyn ModalViewHandle>,
|
||||
subscription: Subscription,
|
||||
_subscription: Subscription,
|
||||
previous_focus_handle: Option<FocusHandle>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ impl ModalLayer {
|
|||
{
|
||||
self.active_modal = Some(ActiveModal {
|
||||
modal: Box::new(new_modal.clone()),
|
||||
subscription: cx.subscribe(&new_modal, |this, modal, _: &DismissEvent, cx| {
|
||||
_subscription: cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
|
||||
this.hide_modal(cx);
|
||||
}),
|
||||
previous_focus_handle: cx.focused(),
|
||||
|
@ -116,7 +116,7 @@ impl Render for ModalLayer {
|
|||
.size_full()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.z_index(400)
|
||||
.z_index(169)
|
||||
.child(
|
||||
v_stack()
|
||||
.h(px(0.0))
|
||||
|
|
|
@ -104,12 +104,9 @@ impl Workspace {
|
|||
})
|
||||
{
|
||||
let notification = build_notification(cx);
|
||||
cx.subscribe(
|
||||
¬ification,
|
||||
move |this, handle, event: &DismissEvent, cx| {
|
||||
this.dismiss_notification_internal(type_id, id, cx);
|
||||
},
|
||||
)
|
||||
cx.subscribe(¬ification, move |this, _, _: &DismissEvent, cx| {
|
||||
this.dismiss_notification_internal(type_id, id, cx);
|
||||
})
|
||||
.detach();
|
||||
self.notifications
|
||||
.push((type_id, id, Box::new(notification)));
|
||||
|
@ -173,21 +170,15 @@ impl Workspace {
|
|||
|
||||
pub mod simple_message_notification {
|
||||
use gpui::{
|
||||
div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
|
||||
ViewContext,
|
||||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
enum NotificationMessage {
|
||||
Text(SharedString),
|
||||
Element(fn(TextStyle, &AppContext) -> AnyElement),
|
||||
}
|
||||
|
||||
pub struct MessageNotification {
|
||||
message: NotificationMessage,
|
||||
message: SharedString,
|
||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
click_message: Option<SharedString>,
|
||||
}
|
||||
|
@ -200,23 +191,12 @@ pub mod simple_message_notification {
|
|||
S: Into<SharedString>,
|
||||
{
|
||||
Self {
|
||||
message: NotificationMessage::Text(message.into()),
|
||||
message: message.into(),
|
||||
on_click: None,
|
||||
click_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
// not needed I think (only for the "new panel" toast, which is outdated now)
|
||||
// pub fn new_element(
|
||||
// message: fn(TextStyle, &AppContext) -> AnyElement,
|
||||
// ) -> MessageNotification {
|
||||
// Self {
|
||||
// message: NotificationMessage::Element(message),
|
||||
// on_click: None,
|
||||
// click_message: None,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn with_click_message<S>(mut self, message: S) -> Self
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
|
@ -248,18 +228,13 @@ pub mod simple_message_notification {
|
|||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(div().max_w_80().child(match &self.message {
|
||||
NotificationMessage::Text(text) => Label::new(text.clone()),
|
||||
NotificationMessage::Element(element) => {
|
||||
todo!()
|
||||
}
|
||||
}))
|
||||
.child(div().max_w_80().child(Label::new(self.message.clone())))
|
||||
.child(
|
||||
div()
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
)
|
||||
.children(self.click_message.iter().map(|message| {
|
||||
|
|
|
@ -7,10 +7,11 @@ use crate::{
|
|||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use gpui::{
|
||||
actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext,
|
||||
AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
|
||||
ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AppContext,
|
||||
AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel,
|
||||
Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
|
@ -28,8 +29,8 @@ use std::{
|
|||
use theme::ThemeSettings;
|
||||
|
||||
use ui::{
|
||||
h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
|
||||
Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
|
||||
prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
|
||||
Tab, TabBar, TabPosition, Tooltip,
|
||||
};
|
||||
use ui::{v_stack, ContextMenu};
|
||||
use util::{maybe, truncate_and_remove_front, ResultExt};
|
||||
|
@ -164,11 +165,6 @@ impl fmt::Debug for Event {
|
|||
}
|
||||
}
|
||||
|
||||
struct FocusedView {
|
||||
view: AnyWeakView,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
pub struct Pane {
|
||||
focus_handle: FocusHandle,
|
||||
items: Vec<Box<dyn ItemHandle>>,
|
||||
|
@ -184,10 +180,11 @@ pub struct Pane {
|
|||
// tab_context_menu: ViewHandle<ContextMenu>,
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
drag_split_direction: Option<SplitDirection>,
|
||||
// can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
|
||||
can_split: bool,
|
||||
// render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
tab_bar_scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
|
@ -231,6 +228,7 @@ pub struct NavigationEntry {
|
|||
pub timestamp: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DraggedTab {
|
||||
pub pane: View<Pane>,
|
||||
pub ix: usize,
|
||||
|
@ -365,6 +363,7 @@ impl Pane {
|
|||
new_item_menu: None,
|
||||
split_item_menu: None,
|
||||
tab_bar_scroll_handle: ScrollHandle::new(),
|
||||
drag_split_direction: None,
|
||||
// tab_bar_context_menu: TabBarContextMenu {
|
||||
// kind: TabBarContextMenuKind::New,
|
||||
// handle: context_menu,
|
||||
|
@ -431,14 +430,10 @@ impl Pane {
|
|||
// })
|
||||
// .into_any()
|
||||
// }),
|
||||
subscriptions,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn workspace(&self) -> &WeakView<Workspace> {
|
||||
&self.workspace
|
||||
}
|
||||
|
||||
pub fn has_focus(&self, cx: &WindowContext) -> bool {
|
||||
// todo!(); // inline this manually
|
||||
self.focus_handle.contains_focused(cx)
|
||||
|
@ -1467,21 +1462,6 @@ impl Pane {
|
|||
let label = item.tab_content(Some(detail), is_active, cx);
|
||||
let close_side = &ItemSettings::get_global(cx).close_position;
|
||||
|
||||
let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
|
||||
false => (
|
||||
cx.theme().colors().text_muted,
|
||||
cx.theme().colors().tab_inactive_background,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
true => (
|
||||
cx.theme().colors().text,
|
||||
cx.theme().colors().tab_active_background,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
|
||||
let indicator = maybe!({
|
||||
let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
|
||||
(true, _) => Color::Warning,
|
||||
|
@ -1497,56 +1477,57 @@ impl Pane {
|
|||
let is_last_item = ix == self.items.len() - 1;
|
||||
let position_relative_to_active_item = ix.cmp(&self.active_item_index);
|
||||
|
||||
let tab =
|
||||
Tab::new(ix)
|
||||
.position(if is_first_item {
|
||||
TabPosition::First
|
||||
} else if is_last_item {
|
||||
TabPosition::Last
|
||||
} else {
|
||||
TabPosition::Middle(position_relative_to_active_item)
|
||||
})
|
||||
.close_side(match close_side {
|
||||
ClosePosition::Left => ui::TabCloseSide::Start,
|
||||
ClosePosition::Right => ui::TabCloseSide::End,
|
||||
})
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(move |pane: &mut Self, event, cx| {
|
||||
pane.activate_item(ix, true, true, cx)
|
||||
}))
|
||||
.on_drag({
|
||||
let pane = cx.view().clone();
|
||||
move |cx| {
|
||||
cx.build_view(|cx| DraggedTab {
|
||||
pane: pane.clone(),
|
||||
detail,
|
||||
item_id,
|
||||
is_active,
|
||||
ix,
|
||||
})
|
||||
}
|
||||
})
|
||||
.drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().tab_active_background))
|
||||
.on_drop(
|
||||
cx.listener(move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||
this.handle_tab_drop(dragged_tab, ix, cx)
|
||||
}),
|
||||
)
|
||||
.when_some(item.tab_tooltip_text(cx), |tab, text| {
|
||||
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
|
||||
})
|
||||
.start_slot::<Indicator>(indicator)
|
||||
.end_slot(
|
||||
IconButton::new("close tab", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(move |pane, _, cx| {
|
||||
pane.close_item_by_id(item_id, SaveIntent::Close, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})),
|
||||
)
|
||||
.child(label);
|
||||
let tab = Tab::new(ix)
|
||||
.position(if is_first_item {
|
||||
TabPosition::First
|
||||
} else if is_last_item {
|
||||
TabPosition::Last
|
||||
} else {
|
||||
TabPosition::Middle(position_relative_to_active_item)
|
||||
})
|
||||
.close_side(match close_side {
|
||||
ClosePosition::Left => ui::TabCloseSide::Start,
|
||||
ClosePosition::Right => ui::TabCloseSide::End,
|
||||
})
|
||||
.selected(is_active)
|
||||
.on_click(
|
||||
cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
|
||||
)
|
||||
.on_drag(
|
||||
DraggedTab {
|
||||
pane: cx.view().clone(),
|
||||
detail,
|
||||
item_id,
|
||||
is_active,
|
||||
ix,
|
||||
},
|
||||
|tab, cx| cx.build_view(|_| tab.clone()),
|
||||
)
|
||||
.drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().tab_active_background))
|
||||
.drag_over::<ProjectEntryId>(|tab| tab.bg(gpui::red()))
|
||||
.on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_tab_drop(dragged_tab, ix, cx)
|
||||
}))
|
||||
.on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_project_entry_drop(entry_id, cx)
|
||||
}))
|
||||
.when_some(item.tab_tooltip_text(cx), |tab, text| {
|
||||
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
|
||||
})
|
||||
.start_slot::<Indicator>(indicator)
|
||||
.end_slot(
|
||||
IconButton::new("close tab", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(move |pane, _, cx| {
|
||||
pane.close_item_by_id(item_id, SaveIntent::Close, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})),
|
||||
)
|
||||
.child(label);
|
||||
|
||||
let single_entry_to_resolve = {
|
||||
let item_entries = self.items[ix].project_entry_ids(cx);
|
||||
|
@ -1597,7 +1578,8 @@ impl Pane {
|
|||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_backward()),
|
||||
.disabled(!self.can_navigate_backward())
|
||||
.tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)),
|
||||
)
|
||||
.start_child(
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||
|
@ -1606,7 +1588,8 @@ impl Pane {
|
|||
let view = cx.view().clone();
|
||||
move |_, cx| view.update(cx, Self::navigate_backward)
|
||||
})
|
||||
.disabled(!self.can_navigate_forward()),
|
||||
.disabled(!self.can_navigate_forward())
|
||||
.tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)),
|
||||
)
|
||||
.end_child(
|
||||
div()
|
||||
|
@ -1614,18 +1597,19 @@ impl Pane {
|
|||
IconButton::new("plus", Icon::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("New File", NewFile.boxed_clone())
|
||||
.action("New Terminal", NewCenterTerminal.boxed_clone())
|
||||
.action("New Search", NewSearch.boxed_clone())
|
||||
});
|
||||
cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
|
||||
cx.subscribe(&menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.new_item_menu = None;
|
||||
})
|
||||
.detach();
|
||||
this.new_item_menu = Some(menu);
|
||||
})),
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("New...", cx)),
|
||||
)
|
||||
.when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
|
||||
el.child(Self::render_menu_overlay(new_item_menu))
|
||||
|
@ -1637,19 +1621,20 @@ impl Pane {
|
|||
IconButton::new("split", Icon::Split)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, cx| {
|
||||
let menu = ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
});
|
||||
cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
|
||||
cx.subscribe(&menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.focus(cx);
|
||||
this.split_item_menu = None;
|
||||
})
|
||||
.detach();
|
||||
this.split_item_menu = Some(menu);
|
||||
})),
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Split Pane", cx)),
|
||||
)
|
||||
.when_some(self.split_item_menu.as_ref(), |el, split_item_menu| {
|
||||
el.child(Self::render_menu_overlay(split_item_menu))
|
||||
|
@ -1664,16 +1649,24 @@ impl Pane {
|
|||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_6()
|
||||
// HACK: This empty child is currently necessary to force the drop traget to appear
|
||||
// despite us setting a min width above.
|
||||
.child("")
|
||||
.h_full()
|
||||
.flex_grow()
|
||||
.drag_over::<DraggedTab>(|bar| {
|
||||
bar.bg(cx.theme().colors().tab_active_background)
|
||||
})
|
||||
.on_drop(
|
||||
cx.listener(move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||
this.handle_tab_drop(dragged_tab, this.items.len(), cx)
|
||||
}),
|
||||
),
|
||||
.drag_over::<ProjectEntryId>(|bar| bar.bg(gpui::red()))
|
||||
.on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_tab_drop(dragged_tab, this.items.len(), cx)
|
||||
}))
|
||||
.on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_project_entry_drop(entry_id, cx)
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1733,24 +1726,75 @@ impl Pane {
|
|||
self.zoomed
|
||||
}
|
||||
|
||||
fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
|
||||
let edge_width = cx.rem_size() * 8;
|
||||
let cursor = event.event.position;
|
||||
let direction = if cursor.x < event.bounds.left() + edge_width {
|
||||
Some(SplitDirection::Left)
|
||||
} else if cursor.x > event.bounds.right() - edge_width {
|
||||
Some(SplitDirection::Right)
|
||||
} else if cursor.y < event.bounds.top() + edge_width {
|
||||
Some(SplitDirection::Up)
|
||||
} else if cursor.y > event.bounds.bottom() - edge_width {
|
||||
Some(SplitDirection::Down)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if direction != self.drag_split_direction {
|
||||
self.drag_split_direction = direction;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tab_drop(
|
||||
&mut self,
|
||||
dragged_tab: &View<DraggedTab>,
|
||||
dragged_tab: &DraggedTab,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) {
|
||||
let dragged_tab = dragged_tab.read(cx);
|
||||
let mut to_pane = cx.view().clone();
|
||||
let split_direction = self.drag_split_direction;
|
||||
let item_id = dragged_tab.item_id;
|
||||
let from_pane = dragged_tab.pane.clone();
|
||||
let to_pane = cx.view().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |_, cx| {
|
||||
cx.defer(move |workspace, cx| {
|
||||
if let Some(split_direction) = split_direction {
|
||||
to_pane = workspace.split_pane(to_pane, split_direction, cx);
|
||||
}
|
||||
workspace.move_item(from_pane, to_pane, item_id, ix, cx);
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn handle_project_entry_drop(
|
||||
&mut self,
|
||||
project_entry_id: &ProjectEntryId,
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) {
|
||||
let mut to_pane = cx.view().clone();
|
||||
let split_direction = self.drag_split_direction;
|
||||
let project_entry_id = *project_entry_id;
|
||||
self.workspace
|
||||
.update(cx, |_, cx| {
|
||||
cx.defer(move |workspace, cx| {
|
||||
if let Some(path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.path_for_entry(project_entry_id, cx)
|
||||
{
|
||||
if let Some(split_direction) = split_direction {
|
||||
to_pane = workspace.split_pane(to_pane, split_direction, cx);
|
||||
}
|
||||
workspace
|
||||
.open_path(path, Some(to_pane.downgrade()), true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for Pane {
|
||||
|
@ -1763,7 +1807,8 @@ impl Render for Pane {
|
|||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let this = cx.view().downgrade();
|
||||
let mut drag_target_color = cx.theme().colors().text;
|
||||
drag_target_color.a = 0.5;
|
||||
|
||||
v_stack()
|
||||
.key_context("Pane")
|
||||
|
@ -1842,16 +1887,60 @@ impl Render for Pane {
|
|||
}),
|
||||
)
|
||||
.child(self.render_tab_bar(cx))
|
||||
.child(self.toolbar.clone())
|
||||
.child(if let Some(item) = self.active_item() {
|
||||
div().flex().flex_1().child(item.to_any())
|
||||
} else {
|
||||
h_stack()
|
||||
.items_center()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Open a file or project to get started.").color(Color::Muted))
|
||||
})
|
||||
.child(
|
||||
// main content
|
||||
div()
|
||||
.flex_1()
|
||||
.relative()
|
||||
.group("")
|
||||
.on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
|
||||
.on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
|
||||
.map(|div| {
|
||||
if let Some(item) = self.active_item() {
|
||||
div.flex_col()
|
||||
.child(self.toolbar.clone())
|
||||
.child(item.to_any())
|
||||
} else {
|
||||
div.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.child(
|
||||
Label::new("Open a file or project to get started.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
// drag target
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.bg(drag_target_color)
|
||||
.group_drag_over::<DraggedTab>("", |style| style.visible())
|
||||
.group_drag_over::<ProjectEntryId>("", |style| style.visible())
|
||||
.on_drop(cx.listener(move |this, dragged_tab, cx| {
|
||||
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
|
||||
}))
|
||||
.on_drop(cx.listener(move |this, entry_id, cx| {
|
||||
this.handle_project_entry_drop(entry_id, cx)
|
||||
}))
|
||||
.map(|div| match self.drag_split_direction {
|
||||
None => div.top_0().left_0().right_0().bottom_0(),
|
||||
Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
|
||||
Some(SplitDirection::Down) => {
|
||||
div.left_0().bottom_0().right_0().h_32()
|
||||
}
|
||||
Some(SplitDirection::Left) => {
|
||||
div.top_0().left_0().bottom_0().w_32()
|
||||
}
|
||||
Some(SplitDirection::Right) => {
|
||||
div.top_0().bottom_0().right_0().w_32()
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Navigate(NavigationDirection::Back),
|
||||
cx.listener(|pane, _, cx| {
|
||||
|
|
|
@ -16,7 +16,7 @@ const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
|
|||
const HORIZONTAL_MIN_SIZE: f32 = 80.;
|
||||
const VERTICAL_MIN_SIZE: f32 = 100.;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone)]
|
||||
pub struct PaneGroup {
|
||||
pub(crate) root: Member,
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ impl PaneGroup {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(View<Pane>),
|
||||
|
@ -426,12 +426,6 @@ pub(crate) struct PaneAxis {
|
|||
pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for PaneAxis {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl PaneAxis {
|
||||
pub fn new(axis: Axis, members: Vec<Member>) -> Self {
|
||||
let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
|
||||
|
@ -816,7 +810,7 @@ mod element {
|
|||
proposed_current_pixel_change -= current_pixel_change;
|
||||
}
|
||||
|
||||
// todo!(reserialize workspace)
|
||||
// todo!(schedule serialize)
|
||||
// workspace.schedule_serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -851,7 +845,7 @@ mod element {
|
|||
|
||||
cx.on_mouse_event({
|
||||
let dragged_handle = dragged_handle.clone();
|
||||
move |e: &MouseDownEvent, phase, cx| {
|
||||
move |e: &MouseDownEvent, phase, _cx| {
|
||||
if phase.bubble() && handle_bounds.contains(&e.position) {
|
||||
dragged_handle.replace(Some(ix));
|
||||
}
|
||||
|
@ -859,7 +853,7 @@ mod element {
|
|||
});
|
||||
cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
|
||||
let dragged_handle = dragged_handle.borrow();
|
||||
if *dragged_handle == Some(ix) {
|
||||
if phase.bubble() && *dragged_handle == Some(ix) {
|
||||
Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
|
||||
}
|
||||
});
|
||||
|
@ -896,7 +890,7 @@ mod element {
|
|||
}
|
||||
|
||||
fn paint(
|
||||
self,
|
||||
&mut self,
|
||||
bounds: gpui::Bounds<ui::prelude::Pixels>,
|
||||
state: &mut Self::State,
|
||||
cx: &mut ui::prelude::WindowContext,
|
||||
|
@ -912,7 +906,7 @@ mod element {
|
|||
let mut bounding_boxes = self.bounding_boxes.lock();
|
||||
bounding_boxes.clear();
|
||||
|
||||
for (ix, child) in self.children.into_iter().enumerate() {
|
||||
for (ix, child) in self.children.iter_mut().enumerate() {
|
||||
//todo!(active_pane_magnification)
|
||||
// If usign active pane magnification, need to switch to using
|
||||
// 1 for all non-active panes, and then the magnification for the
|
||||
|
@ -949,7 +943,7 @@ mod element {
|
|||
cx.with_z_index(1, |cx| {
|
||||
cx.on_mouse_event({
|
||||
let state = state.clone();
|
||||
move |e: &MouseUpEvent, phase, cx| {
|
||||
move |_: &MouseUpEvent, phase, _cx| {
|
||||
if phase.bubble() {
|
||||
state.replace(None);
|
||||
}
|
||||
|
@ -968,402 +962,4 @@ mod element {
|
|||
fn flex_values_in_bounds(flexes: &[f32]) -> bool {
|
||||
(flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
|
||||
}
|
||||
// // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
|
||||
|
||||
// // use gpui::{
|
||||
// // geometry::{
|
||||
// // rect::Bounds<Pixels>,
|
||||
// // vector::{vec2f, Vector2F},
|
||||
// // },
|
||||
// // json::{self, ToJson},
|
||||
// // platform::{CursorStyle, MouseButton},
|
||||
// // scene::MouseDrag,
|
||||
// // AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, Bounds<Pixels>Ext,
|
||||
// // SizeConstraint, Vector2FExt, ViewContext,
|
||||
// // };
|
||||
|
||||
// use crate::{
|
||||
// pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
|
||||
// Workspace, WorkspaceSettings,
|
||||
// };
|
||||
|
||||
// pub struct PaneAxisElement {
|
||||
// axis: Axis,
|
||||
// basis: usize,
|
||||
// active_pane_ix: Option<usize>,
|
||||
// flexes: Rc<RefCell<Vec<f32>>>,
|
||||
// children: Vec<AnyElement<Workspace>>,
|
||||
// bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
|
||||
// }
|
||||
|
||||
// impl PaneAxisElement {
|
||||
// pub fn new(
|
||||
// axis: Axis,
|
||||
// basis: usize,
|
||||
// flexes: Rc<RefCell<Vec<f32>>>,
|
||||
// bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
|
||||
// ) -> Self {
|
||||
// Self {
|
||||
// axis,
|
||||
// basis,
|
||||
// flexes,
|
||||
// bounding_boxes,
|
||||
// active_pane_ix: None,
|
||||
// children: Default::default(),
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
|
||||
// self.active_pane_ix = active_pane_ix;
|
||||
// }
|
||||
|
||||
// fn layout_children(
|
||||
// &mut self,
|
||||
// active_pane_magnification: f32,
|
||||
// constraint: SizeConstraint,
|
||||
// remaining_space: &mut f32,
|
||||
// remaining_flex: &mut f32,
|
||||
// cross_axis_max: &mut f32,
|
||||
// view: &mut Workspace,
|
||||
// cx: &mut ViewContext<Workspace>,
|
||||
// ) {
|
||||
// let flexes = self.flexes.borrow();
|
||||
// let cross_axis = self.axis.invert();
|
||||
// for (ix, child) in self.children.iter_mut().enumerate() {
|
||||
// let flex = if active_pane_magnification != 1. {
|
||||
// if let Some(active_pane_ix) = self.active_pane_ix {
|
||||
// if ix == active_pane_ix {
|
||||
// active_pane_magnification
|
||||
// } else {
|
||||
// 1.
|
||||
// }
|
||||
// } else {
|
||||
// 1.
|
||||
// }
|
||||
// } else {
|
||||
// flexes[ix]
|
||||
// };
|
||||
|
||||
// let child_size = if *remaining_flex == 0.0 {
|
||||
// *remaining_space
|
||||
// } else {
|
||||
// let space_per_flex = *remaining_space / *remaining_flex;
|
||||
// space_per_flex * flex
|
||||
// };
|
||||
|
||||
// let child_constraint = match self.axis {
|
||||
// Axis::Horizontal => SizeConstraint::new(
|
||||
// vec2f(child_size, constraint.min.y()),
|
||||
// vec2f(child_size, constraint.max.y()),
|
||||
// ),
|
||||
// Axis::Vertical => SizeConstraint::new(
|
||||
// vec2f(constraint.min.x(), child_size),
|
||||
// vec2f(constraint.max.x(), child_size),
|
||||
// ),
|
||||
// };
|
||||
// let child_size = child.layout(child_constraint, view, cx);
|
||||
// *remaining_space -= child_size.along(self.axis);
|
||||
// *remaining_flex -= flex;
|
||||
// *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn handle_resize(
|
||||
// flexes: Rc<RefCell<Vec<f32>>>,
|
||||
// axis: Axis,
|
||||
// preceding_ix: usize,
|
||||
// child_start: Vector2F,
|
||||
// drag_bounds: Bounds<Pixels>,
|
||||
// ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
|
||||
// let size = move |ix, flexes: &[f32]| {
|
||||
// drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
|
||||
// };
|
||||
|
||||
// move |drag, workspace: &mut Workspace, cx| {
|
||||
// if drag.end {
|
||||
// // TODO: Clear cascading resize state
|
||||
// return;
|
||||
// }
|
||||
// let min_size = match axis {
|
||||
// Axis::Horizontal => HORIZONTAL_MIN_SIZE,
|
||||
// Axis::Vertical => VERTICAL_MIN_SIZE,
|
||||
// };
|
||||
// let mut flexes = flexes.borrow_mut();
|
||||
|
||||
// // Don't allow resizing to less than the minimum size, if elements are already too small
|
||||
// if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
|
||||
// - size(preceding_ix, flexes.as_slice());
|
||||
|
||||
// let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
|
||||
// let flex_change = pixel_dx / drag_bounds.length_along(axis);
|
||||
// let current_target_flex = flexes[target_ix] + flex_change;
|
||||
// let next_target_flex =
|
||||
// flexes[(target_ix as isize + next) as usize] - flex_change;
|
||||
// (current_target_flex, next_target_flex)
|
||||
// };
|
||||
|
||||
// let mut successors = from_fn({
|
||||
// let forward = proposed_current_pixel_change > 0.;
|
||||
// let mut ix_offset = 0;
|
||||
// let len = flexes.len();
|
||||
// move || {
|
||||
// let result = if forward {
|
||||
// (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
|
||||
// } else {
|
||||
// (preceding_ix as isize - ix_offset as isize >= 0)
|
||||
// .then(|| preceding_ix - ix_offset)
|
||||
// };
|
||||
|
||||
// ix_offset += 1;
|
||||
|
||||
// result
|
||||
// }
|
||||
// });
|
||||
|
||||
// while proposed_current_pixel_change.abs() > 0. {
|
||||
// let Some(current_ix) = successors.next() else {
|
||||
// break;
|
||||
// };
|
||||
|
||||
// let next_target_size = f32::max(
|
||||
// size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
|
||||
// min_size,
|
||||
// );
|
||||
|
||||
// let current_target_size = f32::max(
|
||||
// size(current_ix, flexes.as_slice())
|
||||
// + size(current_ix + 1, flexes.as_slice())
|
||||
// - next_target_size,
|
||||
// min_size,
|
||||
// );
|
||||
|
||||
// let current_pixel_change =
|
||||
// current_target_size - size(current_ix, flexes.as_slice());
|
||||
|
||||
// let (current_target_flex, next_target_flex) =
|
||||
// flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
|
||||
|
||||
// flexes[current_ix] = current_target_flex;
|
||||
// flexes[current_ix + 1] = next_target_flex;
|
||||
|
||||
// proposed_current_pixel_change -= current_pixel_change;
|
||||
// }
|
||||
|
||||
// workspace.schedule_serialize(cx);
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Extend<AnyElement<Workspace>> for PaneAxisElement {
|
||||
// fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
|
||||
// self.children.extend(children);
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Element<Workspace> for PaneAxisElement {
|
||||
// type LayoutState = f32;
|
||||
// type PaintState = ();
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// constraint: SizeConstraint,
|
||||
// view: &mut Workspace,
|
||||
// cx: &mut ViewContext<Workspace>,
|
||||
// ) -> (Vector2F, Self::LayoutState) {
|
||||
// debug_assert!(self.children.len() == self.flexes.borrow().len());
|
||||
|
||||
// let active_pane_magnification =
|
||||
// settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
|
||||
|
||||
// let mut remaining_flex = 0.;
|
||||
|
||||
// if active_pane_magnification != 1. {
|
||||
// let active_pane_flex = self
|
||||
// .active_pane_ix
|
||||
// .map(|_| active_pane_magnification)
|
||||
// .unwrap_or(1.);
|
||||
// remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
|
||||
// } else {
|
||||
// for flex in self.flexes.borrow().iter() {
|
||||
// remaining_flex += flex;
|
||||
// }
|
||||
// }
|
||||
|
||||
// let mut cross_axis_max: f32 = 0.0;
|
||||
// let mut remaining_space = constraint.max_along(self.axis);
|
||||
|
||||
// if remaining_space.is_infinite() {
|
||||
// panic!("flex contains flexible children but has an infinite constraint along the flex axis");
|
||||
// }
|
||||
|
||||
// self.layout_children(
|
||||
// active_pane_magnification,
|
||||
// constraint,
|
||||
// &mut remaining_space,
|
||||
// &mut remaining_flex,
|
||||
// &mut cross_axis_max,
|
||||
// view,
|
||||
// cx,
|
||||
// );
|
||||
|
||||
// let mut size = match self.axis {
|
||||
// Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
|
||||
// Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
|
||||
// };
|
||||
|
||||
// if constraint.min.x().is_finite() {
|
||||
// size.set_x(size.x().max(constraint.min.x()));
|
||||
// }
|
||||
// if constraint.min.y().is_finite() {
|
||||
// size.set_y(size.y().max(constraint.min.y()));
|
||||
// }
|
||||
|
||||
// if size.x() > constraint.max.x() {
|
||||
// size.set_x(constraint.max.x());
|
||||
// }
|
||||
// if size.y() > constraint.max.y() {
|
||||
// size.set_y(constraint.max.y());
|
||||
// }
|
||||
|
||||
// (size, remaining_space)
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: Bounds<Pixels>,
|
||||
// visible_bounds: Bounds<Pixels>,
|
||||
// remaining_space: &mut Self::LayoutState,
|
||||
// view: &mut Workspace,
|
||||
// cx: &mut ViewContext<Workspace>,
|
||||
// ) -> Self::PaintState {
|
||||
// let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
|
||||
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
// let overflowing = *remaining_space < 0.;
|
||||
// if overflowing {
|
||||
// cx.scene().push_layer(Some(visible_bounds));
|
||||
// }
|
||||
|
||||
// let mut child_origin = bounds.origin();
|
||||
|
||||
// let mut bounding_boxes = self.bounding_boxes.borrow_mut();
|
||||
// bounding_boxes.clear();
|
||||
|
||||
// let mut children_iter = self.children.iter_mut().enumerate().peekable();
|
||||
// while let Some((ix, child)) = children_iter.next() {
|
||||
// let child_start = child_origin.clone();
|
||||
// child.paint(child_origin, visible_bounds, view, cx);
|
||||
|
||||
// bounding_boxes.push(Some(Bounds<Pixels>::new(child_origin, child.size())));
|
||||
|
||||
// match self.axis {
|
||||
// Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
// Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
|
||||
// }
|
||||
|
||||
// if can_resize && children_iter.peek().is_some() {
|
||||
// cx.scene().push_stacking_context(None, None);
|
||||
|
||||
// let handle_origin = match self.axis {
|
||||
// Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
|
||||
// Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
|
||||
// };
|
||||
|
||||
// let handle_bounds = match self.axis {
|
||||
// Axis::Horizontal => Bounds<Pixels>::new(
|
||||
// handle_origin,
|
||||
// vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
|
||||
// ),
|
||||
// Axis::Vertical => Bounds<Pixels>::new(
|
||||
// handle_origin,
|
||||
// vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
|
||||
// ),
|
||||
// };
|
||||
|
||||
// let style = match self.axis {
|
||||
// Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
||||
// Axis::Vertical => CursorStyle::ResizeUpDown,
|
||||
// };
|
||||
|
||||
// cx.scene().push_cursor_region(CursorRegion {
|
||||
// bounds: handle_bounds,
|
||||
// style,
|
||||
// });
|
||||
|
||||
// enum ResizeHandle {}
|
||||
// let mut mouse_region = MouseRegion::new::<ResizeHandle>(
|
||||
// cx.view_id(),
|
||||
// self.basis + ix,
|
||||
// handle_bounds,
|
||||
// );
|
||||
// mouse_region = mouse_region
|
||||
// .on_drag(
|
||||
// MouseButton::Left,
|
||||
// Self::handle_resize(
|
||||
// self.flexes.clone(),
|
||||
// self.axis,
|
||||
// ix,
|
||||
// child_start,
|
||||
// visible_bounds.clone(),
|
||||
// ),
|
||||
// )
|
||||
// .on_click(MouseButton::Left, {
|
||||
// let flexes = self.flexes.clone();
|
||||
// move |e, v: &mut Workspace, cx| {
|
||||
// if e.click_count >= 2 {
|
||||
// let mut borrow = flexes.borrow_mut();
|
||||
// *borrow = vec![1.; borrow.len()];
|
||||
// v.schedule_serialize(cx);
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// cx.scene().push_mouse_region(mouse_region);
|
||||
|
||||
// cx.scene().pop_stacking_context();
|
||||
// }
|
||||
// }
|
||||
|
||||
// if overflowing {
|
||||
// cx.scene().pop_layer();
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn rect_for_text_range(
|
||||
// &self,
|
||||
// range_utf16: Range<usize>,
|
||||
// _: Bounds<Pixels>,
|
||||
// _: Bounds<Pixels>,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// view: &Workspace,
|
||||
// cx: &ViewContext<Workspace>,
|
||||
// ) -> Option<Bounds<Pixels>> {
|
||||
// self.children
|
||||
// .iter()
|
||||
// .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
|
||||
// }
|
||||
|
||||
// fn debug(
|
||||
// &self,
|
||||
// bounds: Bounds<Pixels>,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// view: &Workspace,
|
||||
// cx: &ViewContext<Workspace>,
|
||||
// ) -> json::Value {
|
||||
// serde_json::json!({
|
||||
// "type": "PaneAxis",
|
||||
// "bounds": bounds.to_json(),
|
||||
// "axis": self.axis.to_json(),
|
||||
// "flexes": *self.flexes.borrow(),
|
||||
// "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
|
|||
cx: &mut WindowContext,
|
||||
) -> Task<Vec<Box<dyn Any + Send>>> {
|
||||
let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
|
||||
cx.spawn(|cx| async {
|
||||
cx.spawn(|_| async {
|
||||
let matches = matches.await;
|
||||
matches
|
||||
.into_iter()
|
||||
|
@ -253,7 +253,7 @@ pub trait WeakSearchableItemHandle: WeakItemHandle {
|
|||
}
|
||||
|
||||
impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
fn upgrade(&self, _cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.upgrade()?))
|
||||
}
|
||||
|
||||
|
|
|
@ -51,14 +51,14 @@ impl Render for StatusBar {
|
|||
}
|
||||
|
||||
impl StatusBar {
|
||||
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_left_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_stack()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.children(self.left_items.iter().map(|item| item.to_any()))
|
||||
}
|
||||
|
||||
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_right_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
h_stack()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
|
|
|
@ -55,6 +55,12 @@ pub struct Toolbar {
|
|||
}
|
||||
|
||||
impl Toolbar {
|
||||
fn has_any_visible_items(&self) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
|
||||
}
|
||||
|
||||
fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
|
||||
self.items.iter().filter_map(|(item, location)| {
|
||||
if *location == ToolbarItemLocation::PrimaryLeft {
|
||||
|
@ -74,12 +80,28 @@ impl Toolbar {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn secondary_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
|
||||
self.items.iter().filter_map(|(item, location)| {
|
||||
if *location == ToolbarItemLocation::Secondary {
|
||||
Some(item.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Toolbar {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
if !self.has_any_visible_items() {
|
||||
return div();
|
||||
}
|
||||
|
||||
let secondary_item = self.secondary_items().next().map(|item| item.to_any());
|
||||
|
||||
v_stack()
|
||||
.border_b()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
|
@ -87,8 +109,24 @@ impl Render for Toolbar {
|
|||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.children(self.items.iter().map(|(child, _)| child.to_any())),
|
||||
.when(self.left_items().count() > 0, |this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.justify_start()
|
||||
.children(self.left_items().map(|item| item.to_any())),
|
||||
)
|
||||
})
|
||||
.when(self.right_items().count() > 0, |this| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.children(self.right_items().map(|item| item.to_any())),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.children(secondary_item)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.117.0"
|
||||
version = "0.118.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue