diff --git a/Cargo.lock b/Cargo.lock index 033ff8b69c..88340c716b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 2190066df5..3b453527b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/crates/auto_update2/src/auto_update.rs b/crates/auto_update2/src/auto_update.rs index 31e474242a..79269acc27 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -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, 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, 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) { +pub fn check(_: &Check, cx: &mut WindowContext) { if let Some(updater) = AutoUpdater::get(cx) { updater.update(cx, |updater, cx| updater.poll(cx)); } else { diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index 4a2efcf807..8bb08912b0 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -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::().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) { + pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { cx.emit(DismissEvent); } } diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index bf8d5dda77..2c97872989 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -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> { 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::(); + 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) }) } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 674a324d97..fd5c183d2a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.30.1" +version = "0.31.0" publish = false [[bin]] diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 65aced8e7e..da13da8306 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -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" } diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 587efbe95f..f3f2a37171 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -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, cx: &mut ViewContext) -> 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 for ChatPanel {} impl Render for ChatPanel { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index ac7457abe0..4edf5ef35b 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -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) -> 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) -> 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::(button) + .selected(is_selected), + ) .when(section == Section::Channels, |el| { el.drag_over::(|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::(|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) diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index bd0c5d4b07..3087e6812f 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -11,14 +11,8 @@ use ui::{prelude::*, Avatar}; use util::{ResultExt as _, TryFutureExt}; use workspace::ModalView; -pub fn init(cx: &mut AppContext) { - //Picker::::init(cx); - //cx.add_action(ContactFinder::dismiss) -} - pub struct ContactFinder { picker: View>, - 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.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.has_focus = true; - // if cx.is_self_focused() { - // cx.focus(&self.picker) - // } - // } - - // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - // 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, potential_contacts: Arc<[Arc]>, @@ -161,7 +128,6 @@ impl PickerDelegate for ContactFinderDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - //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) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2b931f7085..b127708fa6 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -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, client: Arc, workspace: WeakView, - //branch_popover: Option>, - //project_popover: Option>, - //user_menu: ViewHandle, + branch_popover: Option>, + project_popover: Option, _subscriptions: Vec, } @@ -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) -> Option { @@ -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.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 { + 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, - // ) -> Option> { - // self.branch_popover.as_ref().map(|child| { - // let theme = theme::current(cx).clone(); - // let child = ChildView::new(child, cx); - // let child = MouseEventHandler::new::(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) { + 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, - // ) -> Option> { - // self.project_popover.as_ref().map(|child| { - // let theme = theme::current(cx).clone(); - // let child = ChildView::new(child, cx); - // let child = MouseEventHandler::new::(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) { + 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) { - // 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, + ) -> Option { + 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) { - // 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) -> 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>, - // cx: &mut ViewContext, - // ) -> AnyElement { - // 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::(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::( - // 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) -> AnyElement { - // let titlebar = &theme.titlebar; - // MouseEventHandler::new::(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, - // ) -> Option> { - // 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::(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) -> 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)), + ) + } + } } diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index df81af3e57..6b81998a8a 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -34,7 +34,7 @@ pub fn init(app_state: &Arc, 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); diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index d181509c46..fd675127e4 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -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) }); diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 76085aeca8..e58aa1000d 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -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() diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 307a95b70a..0f1b565b9d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -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, + 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 { 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, 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); diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml index 560c5a307f..9fe125ec57 100644 --- a/crates/feedback2/Cargo.toml +++ b/crates/feedback2/Cargo.toml @@ -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" diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index e8715034c2..22904f3a0a 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -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, email_address_editor: View, - awaiting_submission: bool, - user_submitted: bool, - discarded: bool, + submission_state: Option, + dismiss_modal: bool, character_count: i32, } @@ -66,12 +87,7 @@ impl EventEmitter for FeedbackModal {} impl ModalView for FeedbackModal { fn on_before_dismiss(&mut self, cx: &mut ViewContext) -> 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.awaiting_submission = awaiting_submission; - cx.notify(); - } - - fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext) { - self.user_submitted = user_submitted; - cx.notify(); - } - async fn submit_feedback( feedback_text: &str, email: Option, @@ -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) { + 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) { cx.emit(DismissEvent) } @@ -319,17 +390,9 @@ impl Render for FeedbackModal { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> 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)); +// }); +// })?; +// } +// } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 771b561151..8748a60249 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1091,6 +1091,12 @@ impl AppContext { pub fn has_active_drag(&self) -> bool { self.active_drag.is_some() } + + pub fn active_drag(&self) -> Option<&T> { + self.active_drag + .as_ref() + .and_then(|drag| drag.value.downcast_ref()) + } } impl Context for AppContext { @@ -1241,6 +1247,7 @@ impl DerefMut for GlobalLease { /// within the window or by dragging into the app from the underlying platform. pub struct AnyDrag { pub view: AnyView, + pub value: Box, pub cursor_offset: Point, } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 226a477012..b446c2fe86 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -23,7 +23,7 @@ pub trait IntoElement: Sized { self.into_element().into_any() } - fn draw( + fn draw_and_update_state( self, origin: Point, available_space: Size, @@ -92,7 +92,7 @@ pub trait Element: 'static + IntoElement { cx: &mut WindowContext, ) -> (LayoutId, Self::State); - fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext); + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext); fn into_any(self) -> AnyElement { AnyElement::new(self) @@ -150,8 +150,8 @@ impl Element for Component { } } - fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { - let element = state.rendered_element.take().unwrap(); + fn paint(&mut self, bounds: Bounds, 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, available_space: Size, cx: &mut WindowContext, @@ -465,8 +465,8 @@ impl Element for AnyElement { (layout_id, ()) } - fn paint(self, _: Bounds, _: &mut Self::State, cx: &mut WindowContext) { - self.paint(cx); + fn paint(&mut self, _: Bounds, _: &mut Self::State, cx: &mut WindowContext) { + self.paint(cx) } } @@ -482,48 +482,37 @@ impl IntoElement for AnyElement { } } -// impl Element for Option -// where -// V: 'static, -// E: Element, -// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static, -// { -// type State = Option; +/// The empty element, which renders nothing. +pub type Empty = (); -// fn element_id(&self) -> Option { -// None -// } +impl IntoElement for () { + type Element = Self; -// fn layout( -// &mut self, -// _: Option, -// 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 { + None + } -// fn paint( -// self, -// _bounds: Bounds, -// rendered_element: &mut Self::State, -// cx: &mut WindowContext, -// ) { -// rendered_element.take().unwrap().paint(view_state, cx); -// } -// } + fn into_element(self) -> Self::Element { + self + } +} -// impl RenderOnce for Option -// 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, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + (cx.request_layout(&crate::Style::default(), None), ()) + } + + fn paint( + &mut self, + _bounds: Bounds, + _state: &mut Self::State, + _cx: &mut WindowContext, + ) { + } +} diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs index 287a3b4b5a..d04c65811f 100644 --- a/crates/gpui2/src/elements/canvas.rs +++ b/crates/gpui2/src/elements/canvas.rs @@ -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, &mut WindowContext)) -> Canvas { +pub fn canvas(callback: impl 'static + FnOnce(&Bounds, &mut WindowContext)) -> Canvas { Canvas { - paint_callback: Box::new(callback), + paint_callback: Some(Box::new(callback)), style: StyleRefinement::default(), } } pub struct Canvas { - paint_callback: Box, &mut WindowContext)>, + paint_callback: Option, &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, _: &mut (), cx: &mut WindowContext) { - (self.paint_callback)(bounds, cx) + fn paint(&mut self, bounds: Bounds, style: &mut Style, cx: &mut WindowContext) { + style.paint(bounds, cx, |cx| { + (self.paint_callback.take().unwrap())(&bounds, cx) + }); } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 3ec0a46fcb..1f56f44900 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -5,6 +5,7 @@ use crate::{ MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, }; + use collections::HashMap; use refineable::Refineable; use smallvec::SmallVec; @@ -13,6 +14,7 @@ use std::{ cell::RefCell, cmp::Ordering, fmt::Debug, + marker::PhantomData, mem, rc::Rc, time::Duration, @@ -28,6 +30,301 @@ pub struct GroupStyle { pub style: Box, } +pub struct DragMoveEvent { + pub event: MouseMoveEvent, + pub bounds: Bounds, + drag: PhantomData, +} + +impl DragMoveEvent { + pub fn drag<'b>(&self, cx: &'b AppContext) -> &'b T { + cx.active_drag + .as_ref() + .and_then(|drag| drag.value.downcast_ref::()) + .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") + } +} + +impl Interactivity { + pub fn on_mouse_down( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.visibly_contains(&event.position, cx) + { + (listener)(event, cx) + } + })); + } + + pub fn on_any_mouse_down( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + (listener)(event, cx) + } + })); + } + + pub fn on_mouse_up( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.visibly_contains(&event.position, cx) + { + (listener)(event, cx) + } + })); + } + + pub fn on_any_mouse_up( + &mut self, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + (listener)(event, cx) + } + })); + } + + pub fn on_mouse_down_out( + &mut self, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) { + self.mouse_down_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx) + { + (listener)(event, cx) + } + })); + } + + pub fn on_mouse_up_out( + &mut self, + button: MouseButton, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, + ) { + self.mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !bounds.visibly_contains(&event.position, cx) + { + (listener)(event, cx); + } + })); + } + + pub fn on_mouse_move( + &mut self, + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, + ) { + self.mouse_move_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + (listener)(event, cx); + } + })); + } + + pub fn on_drag_move( + &mut self, + listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, + ) where + T: 'static, + { + self.mouse_move_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Capture + && bounds.drag_target_contains(&event.position, cx) + { + if cx + .active_drag + .as_ref() + .is_some_and(|drag| drag.value.as_ref().type_id() == TypeId::of::()) + { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: bounds.bounds, + drag: PhantomData, + }, + cx, + ); + } + } + })); + } + + pub fn on_scroll_wheel( + &mut self, + listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, + ) { + self.scroll_wheel_listeners + .push(Box::new(move |event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { + (listener)(event, cx); + } + })); + } + + pub fn capture_action( + &mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + (listener)(action, cx) + } + }), + )); + } + + pub fn on_action(&mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) { + self.action_listeners.push(( + TypeId::of::(), + Box::new(move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + (listener)(action, cx) + } + }), + )); + } + + pub fn on_boxed_action( + &mut self, + action: &Box, + listener: impl Fn(&Box, &mut WindowContext) + 'static, + ) { + let action = action.boxed_clone(); + self.action_listeners.push(( + (*action).type_id(), + Box::new(move |_, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(&action, cx) + } + }), + )); + } + + pub fn on_key_down(&mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static) { + self.key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(event, cx) + } + })); + } + + pub fn capture_key_down( + &mut self, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, + ) { + self.key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } + })); + } + + pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + listener(event, cx) + } + })); + } + + pub fn capture_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) { + self.key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } + })); + } + + pub fn on_drop(&mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) { + self.drop_listeners.push(( + TypeId::of::(), + Box::new(move |dragged_value, cx| { + listener(dragged_value.downcast_ref().unwrap(), cx); + }), + )); + } + + pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) + where + Self: Sized, + { + self.click_listeners + .push(Box::new(move |event, cx| listener(event, cx))); + } + + pub fn on_drag( + &mut self, + value: T, + constructor: impl Fn(&T, &mut WindowContext) -> View + 'static, + ) where + Self: Sized, + T: 'static, + W: 'static + Render, + { + debug_assert!( + self.drag_listener.is_none(), + "calling on_drag more than once on the same element is not supported" + ); + self.drag_listener = Some(( + Box::new(value), + Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()), + )); + } + + pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) + where + Self: Sized, + { + debug_assert!( + self.hover_listener.is_none(), + "calling on_hover more than once on the same element is not supported" + ); + self.hover_listener = Some(Box::new(listener)); + } + + pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) + where + Self: Sized, + { + debug_assert!( + self.tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" + ); + self.tooltip_builder = Some(Rc::new(build_tooltip)); + } +} + pub trait InteractiveElement: Sized { fn interactivity(&mut self) -> &mut Interactivity; @@ -85,16 +382,7 @@ pub trait InteractiveElement: Sized { button: MouseButton, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_down_listeners.push(Box::new( - move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.visibly_contains(&event.position, cx) - { - (listener)(event, cx) - } - }, - )); + self.interactivity().on_mouse_down(button, listener); self } @@ -102,13 +390,7 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_down_listeners.push(Box::new( - move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { - (listener)(event, cx) - } - }, - )); + self.interactivity().on_any_mouse_down(listener); self } @@ -117,30 +399,7 @@ pub trait InteractiveElement: Sized { button: MouseButton, listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity() - .mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.visibly_contains(&event.position, cx) - { - (listener)(event, cx) - } - })); - self - } - - fn on_any_mouse_up( - mut self, - listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, - ) -> Self { - self.interactivity() - .mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { - (listener)(event, cx) - } - })); + self.interactivity().on_mouse_up(button, listener); self } @@ -148,14 +407,7 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_down_listeners.push(Box::new( - move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx) - { - (listener)(event, cx) - } - }, - )); + self.interactivity().on_mouse_down_out(listener); self } @@ -164,16 +416,7 @@ pub trait InteractiveElement: Sized { button: MouseButton, listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity() - .mouse_up_listeners - .push(Box::new(move |event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.visibly_contains(&event.position, cx) - { - (listener)(event, cx); - } - })); + self.interactivity().on_mouse_up_out(button, listener); self } @@ -181,13 +424,18 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_move_listeners.push(Box::new( - move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { - (listener)(event, cx); - } - }, - )); + self.interactivity().on_mouse_move(listener); + self + } + + fn on_drag_move( + mut self, + listener: impl Fn(&DragMoveEvent, &mut WindowContext) + 'static, + ) -> Self + where + T: 'static, + { + self.interactivity().on_drag_move(listener); self } @@ -195,13 +443,7 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().scroll_wheel_listeners.push(Box::new( - move |event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) { - (listener)(event, cx); - } - }, - )); + self.interactivity().on_scroll_wheel(listener); self } @@ -210,29 +452,13 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&A, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Capture { - (listener)(action, cx) - } - }), - )); + self.interactivity().capture_action(listener); self } /// Add a listener for the given action, fires during the bubble event phase fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { - self.interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - (listener)(action, cx) - } - }), - )); + self.interactivity().on_action(listener); self } @@ -241,15 +467,7 @@ pub trait InteractiveElement: Sized { action: &Box, listener: impl Fn(&Box, &mut WindowContext) + 'static, ) -> Self { - let action = action.boxed_clone(); - self.interactivity().action_listeners.push(( - (*action).type_id(), - Box::new(move |_, phase, cx| { - if phase == DispatchPhase::Bubble { - (listener)(&action, cx) - } - }), - )); + self.interactivity().on_boxed_action(action, listener); self } @@ -257,13 +475,7 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity() - .key_down_listeners - .push(Box::new(move |event, phase, cx| { - if phase == DispatchPhase::Bubble { - (listener)(event, cx) - } - })); + self.interactivity().on_key_down(listener); self } @@ -271,24 +483,12 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity() - .key_down_listeners - .push(Box::new(move |event, phase, cx| { - if phase == DispatchPhase::Capture { - listener(event, cx) - } - })); + self.interactivity().capture_key_down(listener); self } fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self { - self.interactivity() - .key_up_listeners - .push(Box::new(move |event, phase, cx| { - if phase == DispatchPhase::Bubble { - listener(event, cx) - } - })); + self.interactivity().on_key_up(listener); self } @@ -296,13 +496,7 @@ pub trait InteractiveElement: Sized { mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity() - .key_up_listeners - .push(Box::new(move |event, phase, cx| { - if phase == DispatchPhase::Capture { - listener(event, cx) - } - })); + self.interactivity().capture_key_up(listener); self } @@ -328,16 +522,8 @@ pub trait InteractiveElement: Sized { self } - fn on_drop( - mut self, - listener: impl Fn(&View, &mut WindowContext) + 'static, - ) -> Self { - self.interactivity().drop_listeners.push(( - TypeId::of::(), - Box::new(move |dragged_view, cx| { - listener(&dragged_view.downcast().unwrap(), cx); - }), - )); + fn on_drop(mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) -> Self { + self.interactivity().on_drop(listener); self } } @@ -396,25 +582,21 @@ pub trait StatefulInteractiveElement: InteractiveElement { where Self: Sized, { - self.interactivity() - .click_listeners - .push(Box::new(move |event, cx| listener(event, cx))); + self.interactivity().on_click(listener); self } - fn on_drag(mut self, listener: impl Fn(&mut WindowContext) -> View + 'static) -> Self + fn on_drag( + mut self, + value: T, + constructor: impl Fn(&T, &mut WindowContext) -> View + 'static, + ) -> Self where Self: Sized, + T: 'static, W: 'static + Render, { - debug_assert!( - self.interactivity().drag_listener.is_none(), - "calling on_drag more than once on the same element is not supported" - ); - self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag { - view: listener(cx).into(), - cursor_offset, - })); + self.interactivity().on_drag(value, constructor); self } @@ -422,11 +604,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { where Self: Sized, { - debug_assert!( - self.interactivity().hover_listener.is_none(), - "calling on_hover more than once on the same element is not supported" - ); - self.interactivity().hover_listener = Some(Box::new(listener)); + self.interactivity().on_hover(listener); self } @@ -434,11 +612,7 @@ pub trait StatefulInteractiveElement: InteractiveElement { where Self: Sized, { - debug_assert!( - self.interactivity().tooltip_builder.is_none(), - "calling tooltip more than once on the same element is not supported" - ); - self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip)); + self.interactivity().tooltip(build_tooltip); self } } @@ -474,9 +648,9 @@ pub type ScrollWheelListener = pub type ClickListener = Box; -pub type DragListener = Box, &mut WindowContext) -> AnyDrag + 'static>; +pub type DragListener = Box AnyView + 'static>; -type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static; +type DropListener = Box; pub type TooltipBuilder = Rc AnyView + 'static>; @@ -488,9 +662,20 @@ pub type DragEventListener = Box; +#[track_caller] pub fn div() -> Div { + #[cfg(debug_assertions)] + let interactivity = { + let mut interactivity = Interactivity::default(); + interactivity.location = Some(*core::panic::Location::caller()); + interactivity + }; + + #[cfg(not(debug_assertions))] + let interactivity = Interactivity::default(); + Div { - interactivity: Interactivity::default(), + interactivity, children: SmallVec::default(), } } @@ -551,7 +736,7 @@ impl Element for Div { } fn paint( - self, + &mut self, bounds: Bounds, element_state: &mut Self::State, cx: &mut WindowContext, @@ -595,27 +780,20 @@ impl Element for Div { &mut element_state.interactive_state, cx, |style, scroll_offset, cx| { - if style.visibility == Visibility::Hidden { - return; - } - let z_index = style.z_index.unwrap_or(0); cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - }); - cx.with_z_index(1, |cx| { + style.paint(bounds, cx, |cx| { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { - for child in self.children { + for child in &mut self.children { child.paint(cx); } }) }) }) - }) + }); }) }, ); @@ -641,7 +819,10 @@ pub struct DivState { impl DivState { pub fn is_active(&self) -> bool { - self.interactive_state.pending_mouse_down.borrow().is_some() + self.interactive_state + .pending_mouse_down + .as_ref() + .map_or(false, |pending| pending.borrow().is_some()) } } @@ -668,11 +849,14 @@ pub struct Interactivity { pub key_down_listeners: Vec, pub key_up_listeners: Vec, pub action_listeners: Vec<(TypeId, ActionListener)>, - pub drop_listeners: Vec<(TypeId, Box)>, + pub drop_listeners: Vec<(TypeId, DropListener)>, pub click_listeners: Vec, - pub drag_listener: Option, + pub drag_listener: Option<(Box, DragListener)>, pub hover_listener: Option>, pub tooltip_builder: Option, + + #[cfg(debug_assertions)] + pub location: Option>, } #[derive(Clone, Debug)] @@ -723,7 +907,7 @@ impl Interactivity { } pub fn paint( - mut self, + &mut self, bounds: Bounds, content_size: Size, element_state: &mut InteractiveElementState, @@ -732,6 +916,121 @@ impl Interactivity { ) { let style = self.compute_style(Some(bounds), element_state, cx); + if style.visibility == Visibility::Hidden { + return; + } + + #[cfg(debug_assertions)] + if self.element_id.is_some() + && (style.debug || style.debug_below || cx.has_global::()) + && bounds.contains(&cx.mouse_position()) + { + const FONT_SIZE: crate::Pixels = crate::Pixels(10.); + let element_id = format!("{:?}", self.element_id.as_ref().unwrap()); + let str_len = element_id.len(); + + let render_debug_text = |cx: &mut WindowContext| { + if let Some(text) = cx + .text_system() + .shape_text( + &element_id, + FONT_SIZE, + &[cx.text_style().to_run(str_len)], + None, + ) + .ok() + .map(|mut text| text.pop()) + .flatten() + { + text.paint(bounds.origin, FONT_SIZE, cx).ok(); + + let text_bounds = crate::Bounds { + origin: bounds.origin, + size: text.size(FONT_SIZE), + }; + if self.location.is_some() + && text_bounds.contains(&cx.mouse_position()) + && cx.modifiers().command + { + let command_held = cx.modifiers().command; + cx.on_key_event({ + let text_bounds = text_bounds.clone(); + move |e: &crate::ModifiersChangedEvent, _phase, cx| { + if e.modifiers.command != command_held + && text_bounds.contains(&cx.mouse_position()) + { + cx.notify(); + } + } + }); + + let hovered = bounds.contains(&cx.mouse_position()); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + if phase == DispatchPhase::Capture { + if bounds.contains(&event.position) != hovered { + cx.notify(); + } + } + }); + + cx.on_mouse_event({ + let location = self.location.clone().unwrap(); + let text_bounds = text_bounds.clone(); + move |e: &crate::MouseDownEvent, phase, cx| { + if text_bounds.contains(&e.position) && phase.capture() { + cx.stop_propagation(); + let Ok(dir) = std::env::current_dir() else { + return; + }; + + eprintln!( + "This element is created at:\n{}:{}:{}", + location.file(), + location.line(), + location.column() + ); + + std::process::Command::new("zed") + .arg(format!( + "{}/{}:{}:{}", + dir.to_string_lossy(), + location.file(), + location.line(), + location.column() + )) + .spawn() + .ok(); + } + } + }); + cx.paint_quad(crate::outline( + crate::Bounds { + origin: bounds.origin + + crate::point(crate::px(0.), FONT_SIZE - px(2.)), + size: crate::Size { + width: text_bounds.size.width, + height: crate::px(1.), + }, + }, + crate::red(), + )) + } + } + }; + + cx.with_z_index(1, |cx| { + cx.with_text_style( + Some(crate::TextStyleRefinement { + color: Some(crate::red()), + line_height: Some(FONT_SIZE.into()), + background_color: Some(crate::white()), + ..Default::default() + }), + render_debug_text, + ) + }); + } + if style .background .as_ref() @@ -740,10 +1039,10 @@ impl Interactivity { cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds)) } - let interactive_bounds = Rc::new(InteractiveBounds { + let interactive_bounds = InteractiveBounds { bounds: bounds.intersect(&cx.content_mask().bounds), stacking_order: cx.stacking_order().clone(), - }); + }; if let Some(mouse_cursor) = style.mouse_cursor { let mouse_position = &cx.mouse_position(); @@ -776,28 +1075,28 @@ impl Interactivity { for listener in self.mouse_down_listeners.drain(..) { let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - listener(event, &*interactive_bounds, phase, cx); + listener(event, &interactive_bounds, phase, cx); }) } for listener in self.mouse_up_listeners.drain(..) { let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { - listener(event, &*interactive_bounds, phase, cx); + listener(event, &interactive_bounds, phase, cx); }) } for listener in self.mouse_move_listeners.drain(..) { let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - listener(event, &*interactive_bounds, phase, cx); + listener(event, &interactive_bounds, phase, cx); }) } for listener in self.scroll_wheel_listeners.drain(..) { let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - listener(event, &*interactive_bounds, phase, cx); + listener(event, &interactive_bounds, phase, cx); }) } @@ -840,8 +1139,10 @@ impl Interactivity { if phase == DispatchPhase::Bubble && interactive_bounds.drag_target_contains(&event.position, cx) { - if let Some(drag_state_type) = - cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) + if let Some(drag_state_type) = cx + .active_drag + .as_ref() + .map(|drag| drag.value.as_ref().type_id()) { for (drop_state_type, listener) in &drop_listeners { if *drop_state_type == drag_state_type { @@ -850,7 +1151,7 @@ impl Interactivity { .take() .expect("checked for type drag state type above"); - listener(drag.view.clone(), cx); + listener(drag.value.as_ref(), cx); cx.notify(); cx.stop_propagation(); } @@ -864,14 +1165,20 @@ impl Interactivity { } let click_listeners = mem::take(&mut self.click_listeners); - let drag_listener = mem::take(&mut self.drag_listener); + let mut drag_listener = mem::take(&mut self.drag_listener); if !click_listeners.is_empty() || drag_listener.is_some() { - let pending_mouse_down = element_state.pending_mouse_down.clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); let mouse_down = pending_mouse_down.borrow().clone(); if let Some(mouse_down) = mouse_down { - if let Some(drag_listener) = drag_listener { - let active_state = element_state.clicked_state.clone(); + if drag_listener.is_some() { + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { @@ -883,10 +1190,18 @@ impl Interactivity { && interactive_bounds.visibly_contains(&event.position, cx) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { + let (drag_value, drag_listener) = drag_listener + .take() + .expect("The notify below should invalidate this callback"); + *active_state.borrow_mut() = ElementClickedState::default(); let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(cursor_offset, cx); - cx.active_drag = Some(drag); + let drag = (drag_listener)(drag_value.as_ref(), cx); + cx.active_drag = Some(AnyDrag { + view: drag, + value: drag_value, + cursor_offset, + }); cx.notify(); cx.stop_propagation(); } @@ -924,8 +1239,14 @@ impl Interactivity { } if let Some(hover_listener) = self.hover_listener.take() { - let was_hovered = element_state.hover_state.clone(); - let has_mouse_down = element_state.pending_mouse_down.clone(); + let was_hovered = element_state + .hover_state + .get_or_insert_with(Default::default) + .clone(); + let has_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { @@ -946,8 +1267,14 @@ impl Interactivity { } if let Some(tooltip_builder) = self.tooltip_builder.take() { - let active_tooltip = element_state.active_tooltip.clone(); - let pending_mouse_down = element_state.pending_mouse_down.clone(); + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); + let pending_mouse_down = element_state + .pending_mouse_down + .get_or_insert_with(Default::default) + .clone(); let interactive_bounds = interactive_bounds.clone(); cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { @@ -989,19 +1316,30 @@ impl Interactivity { } }); - let active_tooltip = element_state.active_tooltip.clone(); + let active_tooltip = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .clone(); cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { active_tooltip.borrow_mut().take(); }); - if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() { + if let Some(active_tooltip) = element_state + .active_tooltip + .get_or_insert_with(Default::default) + .borrow() + .as_ref() + { if active_tooltip.tooltip.is_some() { cx.active_tooltip = active_tooltip.tooltip.clone() } } } - let active_state = element_state.clicked_state.clone(); + let active_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .clone(); if active_state.borrow().is_clicked() { cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { @@ -1077,23 +1415,26 @@ impl Interactivity { .as_ref() .map(|scroll_offset| *scroll_offset.borrow()); + let key_down_listeners = mem::take(&mut self.key_down_listeners); + let key_up_listeners = mem::take(&mut self.key_up_listeners); + let action_listeners = mem::take(&mut self.action_listeners); cx.with_key_dispatch( self.key_context.clone(), element_state.focus_handle.clone(), |_, cx| { - for listener in self.key_down_listeners.drain(..) { + for listener in key_down_listeners { cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { listener(event, phase, cx); }) } - for listener in self.key_up_listeners.drain(..) { + for listener in key_up_listeners { cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { listener(event, phase, cx); }) } - for (action_type, listener) in self.action_listeners { + for (action_type, listener) in action_listeners { cx.on_action(action_type, listener) } @@ -1115,78 +1456,87 @@ impl Interactivity { let mut style = Style::default(); style.refine(&self.base_style); - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() { - if focus_handle.within_focused(cx) { - style.refine(in_focus_style); + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() { + if focus_handle.within_focused(cx) { + style.refine(in_focus_style); + } } - } - if let Some(focus_style) = self.focus_style.as_ref() { - if focus_handle.is_focused(cx) { - style.refine(focus_style); - } - } - } - - if let Some(bounds) = bounds { - let mouse_position = cx.mouse_position(); - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) - { - style.refine(&group_hover.style); + if let Some(focus_style) = self.focus_style.as_ref() { + if focus_handle.is_focused(cx) { + style.refine(focus_style); } } } - if let Some(hover_style) = self.hover_style.as_ref() { - if bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) - { - style.refine(hover_style); - } - } - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains(&mouse_position) + if let Some(bounds) = bounds { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) { - style.refine(&group_drag_style.style); + style.refine(&group_hover.style); } } } - - for (state_type, drag_over_style) in &self.drag_over_styles { - if *state_type == drag.view.entity_type() - && bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) + if let Some(hover_style) = self.hover_style.as_ref() { + if bounds + .intersect(&cx.content_mask().bounds) + .contains(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) { - style.refine(drag_over_style); + style.refine(hover_style); } } - cx.active_drag = Some(drag); - } - } + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.value.as_ref().type_id() + && group_bounds.contains(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } - let clicked_state = element_state.clicked_state.borrow(); - if clicked_state.group { - if let Some(group) = self.group_active_style.as_ref() { - style.refine(&group.style) - } - } + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.value.as_ref().type_id() + && bounds + .intersect(&cx.content_mask().bounds) + .contains(&mouse_position) + && cx.was_top_layer_under_active_drag( + &mouse_position, + cx.stacking_order(), + ) + { + style.refine(drag_over_style); + } + } - if let Some(active_style) = self.active_style.as_ref() { - if clicked_state.element { - style.refine(active_style) + cx.active_drag = Some(drag); + } } - } + + let clicked_state = element_state + .clicked_state + .get_or_insert_with(Default::default) + .borrow(); + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if let Some(active_style) = self.active_style.as_ref() { + if clicked_state.element { + style.refine(active_style) + } + } + }); style } @@ -1223,6 +1573,9 @@ impl Default for Interactivity { drag_listener: None, hover_listener: None, tooltip_builder: None, + + #[cfg(debug_assertions)] + location: None, } } } @@ -1230,11 +1583,11 @@ impl Default for Interactivity { #[derive(Default)] pub struct InteractiveElementState { pub focus_handle: Option, - pub clicked_state: Rc>, - pub hover_state: Rc>, - pub pending_mouse_down: Rc>>, + pub clicked_state: Option>>, + pub hover_state: Option>>, + pub pending_mouse_down: Option>>>, pub scroll_offset: Option>>>, - pub active_tooltip: Rc>>, + pub active_tooltip: Option>>>, } pub struct ActiveTooltip { @@ -1320,7 +1673,7 @@ where self.element.layout(state, cx) } - fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { self.element.paint(bounds, state, cx) } } @@ -1394,7 +1747,7 @@ where self.element.layout(state, cx) } - fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { self.element.paint(bounds, state, cx) } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index f6aae2de66..4f81f604c8 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -81,11 +81,12 @@ impl Element for Img { } fn paint( - self, + &mut self, bounds: Bounds, 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 diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index ba479e1ea8..6818c5c7a2 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -257,7 +257,7 @@ impl Element for List { } fn paint( - self, + &mut self, bounds: crate::Bounds, _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; diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index e925d03d27..5b72019f17 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -81,7 +81,7 @@ impl Element for Overlay { } fn paint( - self, + &mut self, bounds: crate::Bounds, 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); } }) diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index aba31686f5..9ca9baf470 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -36,8 +36,12 @@ impl Element for Svg { }) } - fn paint(self, bounds: Bounds, element_state: &mut Self::State, cx: &mut WindowContext) - where + fn paint( + &mut self, + bounds: Bounds, + element_state: &mut Self::State, + cx: &mut WindowContext, + ) where Self: Sized, { self.interactivity diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index b8fe5e6866..175a79c19a 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -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, state: &mut TextState, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, 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, state: &mut TextState, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, 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, state: &mut Self::State, cx: &mut WindowContext) { + fn paint(&mut self, bounds: Bounds, 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, state: &mut Self::State, cx: &mut WindowContext) { - if let Some(click_listener) = self.click_listener { + fn paint(&mut self, bounds: Bounds, 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, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index d27b3bdb77..9fedbad41c 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -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( view: View, 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, 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); - } - }); - }); - } + }); }) }, ); diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index ee2f42d2a2..f58435d7b9 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -1592,6 +1592,17 @@ impl Edges { } } +impl Into> for f32 { + fn into(self) -> Edges { + 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 Copy for Corners where T: Copy + Clone + Default + Debug {} +impl Into> for f32 { + fn into(self) -> Corners { + Corners { + top_left: self.into(), + top_right: self.into(), + bottom_right: self.into(), + bottom_left: self.into(), + } + } +} + +impl Into> for Pixels { + fn into(self) -> Corners { + 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 diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index b9d5a9f222..e7ee5f9e29 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -147,6 +147,7 @@ pub trait PlatformWindow { fn appearance(&self) -> WindowAppearance; fn display(&self) -> Rc; fn mouse_position(&self) -> Point; + fn modifiers(&self) -> Modifiers; fn as_any_mut(&mut self) -> &mut dyn Any; fn set_input_handler(&mut self, input_handler: Box); fn clear_input_handler(&mut self); diff --git a/crates/gpui2/src/platform/mac/metal_renderer.rs b/crates/gpui2/src/platform/mac/metal_renderer.rs index c477440df5..3210a53c63 100644 --- a/crates/gpui2/src/platform/mac/metal_renderer.rs +++ b/crates/gpui2/src/platform/mac/metal_renderer.rs @@ -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 = 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(); } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index dcdf616ffe..12189e198a 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -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 } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index 5af990514f..0f981d4478 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -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 } diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index ca0a50546e..68c068dfe9 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -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, splitter: BspSplitter<(PrimitiveKind, usize)>, shadows: Vec, @@ -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 { diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 04f247d076..f777e2d8cf 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -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