Merge remote-tracking branch 'origin/main' into perf-2

This commit is contained in:
Nathan Sobo 2023-12-14 14:16:59 -07:00
commit fb3382bcc5
68 changed files with 2701 additions and 1149 deletions

143
Cargo.lock generated
View file

@ -200,7 +200,7 @@ dependencies = [
"toml 0.7.8", "toml 0.7.8",
"unicode-width", "unicode-width",
"vte", "vte",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -293,7 +293,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -303,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -580,7 +580,7 @@ dependencies = [
"futures-lite", "futures-lite",
"rustix 0.37.23", "rustix 0.37.23",
"signal-hook", "signal-hook",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1753,7 +1753,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.30.1" version = "0.31.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1982,6 +1982,7 @@ dependencies = [
"tree-sitter-markdown", "tree-sitter-markdown",
"ui2", "ui2",
"util", "util",
"vcs_menu2",
"workspace2", "workspace2",
"zed_actions2", "zed_actions2",
] ]
@ -2084,6 +2085,19 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "const-cstr" name = "const-cstr"
version = "0.3.0" version = "0.3.0"
@ -2571,7 +2585,7 @@ dependencies = [
"openssl-sys", "openssl-sys",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -2780,6 +2794,20 @@ dependencies = [
"workspace2", "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]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@ -3025,6 +3053,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@ -3103,7 +3137,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [ dependencies = [
"errno-dragonfly", "errno-dragonfly",
"libc", "libc",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -3134,7 +3168,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"home", "home",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -3265,6 +3299,7 @@ dependencies = [
"serde_derive", "serde_derive",
"settings2", "settings2",
"smallvec", "smallvec",
"smol",
"sysinfo", "sysinfo",
"theme2", "theme2",
"tree-sitter-markdown", "tree-sitter-markdown",
@ -3341,7 +3376,7 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -3720,6 +3755,15 @@ dependencies = [
"util", "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]] [[package]]
name = "fuzzy2" name = "fuzzy2"
version = "0.1.0" version = "0.1.0"
@ -4241,7 +4285,7 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -4518,7 +4562,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [ dependencies = [
"hermit-abi 0.3.3", "hermit-abi 0.3.3",
"libc", "libc",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -4575,7 +4619,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi 0.3.3", "hermit-abi 0.3.3",
"rustix 0.38.14", "rustix 0.38.14",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -4998,7 +5042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -5491,7 +5535,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -6510,6 +6554,7 @@ dependencies = [
"theme2", "theme2",
"ui2", "ui2",
"util", "util",
"workspace2",
] ]
[[package]] [[package]]
@ -6671,7 +6716,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"pin-project-lite 0.2.13", "pin-project-lite 0.2.13",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -7019,6 +7064,29 @@ dependencies = [
"workspace", "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]] [[package]]
name = "prometheus" name = "prometheus"
version = "0.13.3" version = "0.13.3"
@ -7954,7 +8022,7 @@ dependencies = [
"io-lifetimes 1.0.11", "io-lifetimes 1.0.11",
"libc", "libc",
"linux-raw-sys 0.3.8", "linux-raw-sys 0.3.8",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -7967,7 +8035,7 @@ dependencies = [
"errno 0.3.3", "errno 0.3.3",
"libc", "libc",
"linux-raw-sys 0.4.7", "linux-raw-sys 0.4.7",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -8080,7 +8148,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -8680,6 +8748,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "2.1.2" version = "2.1.2"
@ -8879,7 +8953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -9195,6 +9269,7 @@ dependencies = [
"backtrace-on-stack-overflow", "backtrace-on-stack-overflow",
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"dialoguer",
"editor2", "editor2",
"fuzzy2", "fuzzy2",
"gpui2", "gpui2",
@ -9495,7 +9570,7 @@ dependencies = [
"fastrand 2.0.0", "fastrand 2.0.0",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix 0.38.14", "rustix 0.38.14",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -9934,7 +10009,7 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.4", "socket2 0.5.4",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -10848,6 +10923,20 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "vcs_menu2"
version = "0.1.0"
dependencies = [
"anyhow",
"fs2",
"fuzzy2",
"gpui2",
"picker2",
"ui2",
"util",
"workspace2",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -11550,6 +11639,15 @@ dependencies = [
"windows-targets 0.48.5", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -11689,7 +11787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -12081,6 +12179,7 @@ dependencies = [
"postage", "postage",
"project2", "project2",
"project_panel2", "project_panel2",
"project_symbols2",
"quick_action_bar2", "quick_action_bar2",
"rand 0.8.5", "rand 0.8.5",
"recent_projects2", "recent_projects2",

View file

@ -90,6 +90,7 @@ members = [
"crates/project_panel", "crates/project_panel",
"crates/project_panel2", "crates/project_panel2",
"crates/project_symbols", "crates/project_symbols",
"crates/project_symbols2",
"crates/quick_action_bar2", "crates/quick_action_bar2",
"crates/recent_projects", "crates/recent_projects",
"crates/recent_projects2", "crates/recent_projects2",
@ -122,6 +123,7 @@ members = [
"crates/story", "crates/story",
"crates/vim", "crates/vim",
"crates/vcs_menu", "crates/vcs_menu",
"crates/vcs_menu2",
"crates/workspace2", "crates/workspace2",
"crates/welcome", "crates/welcome",
"crates/welcome2", "crates/welcome2",

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.30.1" version = "0.31.0"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -47,7 +47,7 @@ settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2"} feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
theme = { package = "theme2", path = "../theme2" } theme = { package = "theme2", path = "../theme2" }
# theme_selector = { path = "../theme_selector" } # theme_selector = { path = "../theme_selector" }
# vcs_menu = { path = "../vcs_menu" } vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
ui = { package = "ui2", path = "../ui2" } ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" } util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" } workspace = { package = "workspace2", path = "../workspace2" }

View file

@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
use std::sync::Arc; use std::sync::Arc;
use theme::ActiveTheme as _; use theme::ActiveTheme as _;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{ use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
IconButton, Label, Tooltip,
};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -382,13 +379,18 @@ impl ChatPanel {
.child(text.element("body".into(), cx)) .child(text.element("body".into(), cx))
.child( .child(
div() div()
.invisible()
.absolute() .absolute()
.top_1() .top_1()
.right_2() .right_2()
.w_8() .w_8()
.group_hover("", |this| this.visible()) .visible_on_hover("")
.child(render_remove(message_id_to_remove, cx)), .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() .into_any()
} }
@ -528,18 +530,6 @@ impl ChatPanel {
} }
} }
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
if let Some(message_id) = message_id_to_remove {
IconButton::new(("remove", message_id), Icon::XCircle)
.on_click(cx.listener(move |this, _, cx| {
this.remove_message(message_id, cx);
}))
.into_any_element()
} else {
div().into_any_element()
}
}
impl EventEmitter<Event> for ChatPanel {} impl EventEmitter<Event> for ChatPanel {}
impl Render for ChatPanel { impl Render for ChatPanel {

View file

@ -175,12 +175,12 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json, actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
ViewContext, VisualContext, WeakView, Subscription, Task, View, ViewContext, VisualContext, WeakView,
}; };
use project::{Fs, Project}; use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -402,7 +402,7 @@ impl CollabPanel {
let filter_editor = cx.build_view(|cx| { let filter_editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx); let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Filter channels, contacts", cx); editor.set_placeholder_text("Filter...", cx);
editor editor
}); });
@ -1156,25 +1156,21 @@ impl CollabPanel {
let tooltip = format!("Follow {}", user.github_login); let tooltip = format!("Follow {}", user.github_login);
ListItem::new(SharedString::from(user.github_login.clone())) ListItem::new(SharedString::from(user.github_login.clone()))
.left_child(Avatar::new(user.avatar_uri.clone())) .start_slot(Avatar::new(user.avatar_uri.clone()))
.child( .child(Label::new(user.github_login.clone()))
h_stack() .end_slot(if is_pending {
.w_full() Label::new("Calling").color(Color::Muted).into_any_element()
.justify_between() } else if is_current_user {
.child(Label::new(user.github_login.clone())) IconButton::new("leave-call", Icon::Exit)
.child(if is_pending { .style(ButtonStyle::Subtle)
Label::new("Calling").color(Color::Muted).into_any_element() .on_click(cx.listener(move |this, _, cx| {
} else if is_current_user { Self::leave_call(cx);
IconButton::new("leave-call", Icon::ArrowRight) }))
.on_click(cx.listener(move |this, _, cx| { .tooltip(|cx| Tooltip::text("Leave Call", cx))
Self::leave_call(cx); .into_any_element()
})) } else {
.tooltip(|cx| Tooltip::text("Leave Call", cx)) div().into_any_element()
.into_any_element() })
} else {
div().into_any_element()
}),
)
.when_some(peer_id, |this, peer_id| { .when_some(peer_id, |this, peer_id| {
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
@ -1212,8 +1208,12 @@ impl CollabPanel {
.detach_and_log_err(cx); .detach_and_log_err(cx);
}); });
})) }))
.left_child(render_tree_branch(is_last, cx)) .start_slot(
.child(IconButton::new(0, Icon::Folder)) h_stack()
.gap_1()
.child(render_tree_branch(is_last, cx))
.child(IconButton::new(0, Icon::Folder)),
)
.child(Label::new(project_name.clone())) .child(Label::new(project_name.clone()))
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) .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); let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
ListItem::new(("screen", id)) ListItem::new(("screen", id))
.left_child(render_tree_branch(is_last, cx)) .start_slot(
.child(IconButton::new(0, Icon::Screen)) h_stack()
.gap_1()
.child(render_tree_branch(is_last, cx))
.child(IconButton::new(0, Icon::Screen)),
)
.child(Label::new("Screen")) .child(Label::new("Screen"))
.when_some(peer_id, |this, _| { .when_some(peer_id, |this, _| {
this.on_click(cx.listener(move |this, _, cx| { this.on_click(cx.listener(move |this, _, cx| {
@ -1372,9 +1376,13 @@ impl CollabPanel {
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.open_channel_notes(channel_id, cx); this.open_channel_notes(channel_id, cx);
})) }))
.left_child(render_tree_branch(false, cx)) .start_slot(
.child(IconButton::new(0, Icon::File)) h_stack()
.child(Label::new("notes")) .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)) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
} }
@ -1387,8 +1395,12 @@ impl CollabPanel {
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx); this.join_channel_chat(channel_id, cx);
})) }))
.left_child(render_tree_branch(true, cx)) .start_slot(
.child(IconButton::new(0, Icon::MessageBubbles)) h_stack()
.gap_1()
.child(render_tree_branch(false, cx))
.child(IconButton::new(0, Icon::MessageBubbles)),
)
.child(Label::new("chat")) .child(Label::new("chat"))
.tooltip(move |cx| Tooltip::text("Open Chat", cx)) .tooltip(move |cx| Tooltip::text("Open Chat", cx))
} }
@ -2149,11 +2161,6 @@ impl CollabPanel {
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div { fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
v_stack() v_stack()
.size_full() .size_full()
.child(
div()
.p_2()
.child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
)
.child( .child(
v_stack() v_stack()
.size_full() .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( fn render_header(
@ -2275,21 +2290,27 @@ impl CollabPanel {
Section::ActiveCall => channel_link.map(|channel_link| { Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link.clone(); let channel_link_copy = channel_link.clone();
IconButton::new("channel-link", Icon::Copy) IconButton::new("channel-link", Icon::Copy)
.icon_size(IconSize::Small)
.size(ButtonSize::None)
.visible_on_hover("section-header")
.on_click(move |_, cx| { .on_click(move |_, cx| {
let item = ClipboardItem::new(channel_link_copy.clone()); let item = ClipboardItem::new(channel_link_copy.clone());
cx.write_to_clipboard(item) cx.write_to_clipboard(item)
}) })
.tooltip(|cx| Tooltip::text("Copy channel link", cx)) .tooltip(|cx| Tooltip::text("Copy channel link", cx))
.into_any_element()
}), }),
Section::Contacts => Some( Section::Contacts => Some(
IconButton::new("add-contact", Icon::Plus) IconButton::new("add-contact", Icon::Plus)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
.tooltip(|cx| Tooltip::text("Search for new contact", cx)), .tooltip(|cx| Tooltip::text("Search for new contact", cx))
.into_any_element(),
), ),
Section::Channels => Some( Section::Channels => Some(
IconButton::new("add-channel", Icon::Plus) IconButton::new("add-channel", Icon::Plus)
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) .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, _ => None,
}; };
@ -2304,25 +2325,18 @@ impl CollabPanel {
h_stack() h_stack()
.w_full() .w_full()
.map(|el| { .group("section-header")
if can_collapse { .child(
el.child( ListHeader::new(text)
ListItem::new(text.clone()) .toggle(if can_collapse {
.child(div().w_full().child(Label::new(text))) Some(!is_collapsed)
.selected(is_selected) } else {
.toggle(Some(!is_collapsed)) None
.on_click(cx.listener(move |this, _, cx| { })
this.toggle_section_expanded(section, cx) .inset(true)
})), .end_slot::<AnyElement>(button)
) .selected(is_selected),
} else { )
el.child(
ListHeader::new(text)
.when_some(button, |el, button| el.meta(button))
.selected(is_selected),
)
}
})
.when(section == Section::Channels, |el| { .when(section == Section::Channels, |el| {
el.drag_over::<DraggedChannelView>(|style| { el.drag_over::<DraggedChannelView>(|style| {
style.bg(cx.theme().colors().ghost_element_hover) style.bg(cx.theme().colors().ghost_element_hover)
@ -2363,25 +2377,20 @@ impl CollabPanel {
}) })
.when(!calling, |el| { .when(!calling, |el| {
el.child( el.child(
div() IconButton::new("remove_contact", Icon::Close)
.id("remove_contact") .icon_color(Color::Muted)
.invisible() .visible_on_hover("")
.group_hover("", |style| style.visible()) .tooltip(|cx| Tooltip::text("Remove Contact", cx))
.child( .on_click(cx.listener({
IconButton::new("remove_contact", Icon::Close) let github_login = github_login.clone();
.icon_color(Color::Muted) move |this, _, cx| {
.tooltip(|cx| Tooltip::text("Remove Contact", cx)) this.remove_contact(user_id, &github_login, 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 // todo!() handle contacts with no avatar
Avatar::new(contact.user.avatar_uri.clone()) Avatar::new(contact.user.avatar_uri.clone())
.availability_indicator(if online { Some(!busy) } else { None }), .availability_indicator(if online { Some(!busy) } else { None }),
@ -2460,7 +2469,7 @@ impl CollabPanel {
.child(Label::new(github_login.clone())) .child(Label::new(github_login.clone()))
.child(h_stack().children(controls)), .child(h_stack().children(controls)),
) )
.left_avatar(user.avatar_uri.clone()) .start_slot(Avatar::new(user.avatar_uri.clone()))
} }
fn render_contact_placeholder( fn render_contact_placeholder(
@ -2541,6 +2550,8 @@ impl CollabPanel {
div() div()
.id(channel_id as usize) .id(channel_id as usize)
.group("") .group("")
.flex()
.w_full()
.on_drag({ .on_drag({
let channel = channel.clone(); let channel = channel.clone();
move |cx| { move |cx| {
@ -2566,67 +2577,10 @@ impl CollabPanel {
) )
.child( .child(
ListItem::new(channel_id as usize) 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 .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) .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) .toggle(disclosed)
.on_toggle( .on_toggle(
cx.listener(move |this, _, cx| { cx.listener(move |this, _, cx| {
@ -2646,7 +2600,49 @@ impl CollabPanel {
move |this, event: &MouseDownEvent, cx| { move |this, event: &MouseDownEvent, cx| {
this.deploy_channel_context_menu(event.position, channel_id, ix, 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)) .tooltip(|cx| Tooltip::text("Join channel", cx))
@ -2962,7 +2958,11 @@ impl CollabPanel {
let item = ListItem::new("channel-editor") let item = ListItem::new("channel-editor")
.inset(false) .inset(false)
.indent_level(depth) .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 if let Some(pending_name) = self
.channel_editing_state .channel_editing_state
@ -2994,7 +2994,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
let right = bounds.right(); let right = bounds.right();
let top = bounds.top(); let top = bounds.top();
cx.paint_quad( cx.paint_quad(fill(
Bounds::from_corners( Bounds::from_corners(
point(start_x, top), point(start_x, top),
point( point(
@ -3002,18 +3002,12 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
if is_last { start_y } else { bounds.bottom() }, if is_last { start_y } else { bounds.bottom() },
), ),
), ),
Default::default(),
color, color,
Default::default(), ));
Hsla::transparent_black(), cx.paint_quad(fill(
);
cx.paint_quad(
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
Default::default(),
color, color,
Default::default(), ));
Hsla::transparent_black(),
);
}) })
.w(width) .w(width)
.h(line_height) .h(line_height)

View file

@ -2,10 +2,10 @@ use crate::face_pile::FacePile;
use call::{ActiveCall, ParticipantLocation, Room}; use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore}; use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
use gpui::{ use gpui::{
actions, canvas, div, overlay, point, px, rems, AppContext, DismissEvent, Div, Element, actions, canvas, div, overlay, point, px, rems, AnyElement, AppContext, DismissEvent, Div,
FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
WeakView, WindowBounds, VisualContext, WeakView, WindowBounds,
}; };
use project::{Project, RepositoryEntry}; use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects; use recent_projects::RecentProjects;
@ -13,9 +13,10 @@ use std::sync::Arc;
use theme::{ActiveTheme, PlayerColors}; use theme::{ActiveTheme, PlayerColors};
use ui::{ use ui::{
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
IconButton, IconElement, KeyBinding, Tooltip, IconButton, IconElement, Tooltip,
}; };
use util::ResultExt; use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB}; use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB};
const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_PROJECT_NAME_LENGTH: usize = 40;
@ -50,7 +51,7 @@ pub struct CollabTitlebarItem {
user_store: Model<UserStore>, user_store: Model<UserStore>,
client: Arc<Client>, client: Arc<Client>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
//branch_popover: Option<ViewHandle<BranchList>>, branch_popover: Option<View<BranchList>>,
project_popover: Option<recent_projects::RecentProjects>, project_popover: Option<recent_projects::RecentProjects>,
//user_menu: ViewHandle<ContextMenu>, //user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@ -329,7 +330,7 @@ impl CollabTitlebarItem {
// menu.set_position_mode(OverlayPositionMode::Local); // menu.set_position_mode(OverlayPositionMode::Local);
// menu // menu
// }), // }),
// branch_popover: None, branch_popover: None,
project_popover: None, project_popover: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@ -408,23 +409,25 @@ impl CollabTitlebarItem {
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
Some( Some(
div().border().border_color(gpui::red()).child( div()
Button::new("project_branch_trigger", branch_name) .border()
.style(ButtonStyle::Subtle) .border_color(gpui::red())
.tooltip(move |cx| { .child(
cx.build_view(|_| { Button::new("project_branch_trigger", branch_name)
Tooltip::new("Recent Branches") .style(ButtonStyle::Subtle)
.key_binding(KeyBinding::new(gpui::KeyBinding::new( .tooltip(move |cx| {
"cmd-b", Tooltip::with_meta(
// todo!() Replace with real action. "Recent Branches",
gpui::NoAction, Some(&ToggleVcsMenu),
None, "Local branches only",
))) cx,
.meta("Local branches only") )
}) })
.into() .on_click(
}), cx.listener(|this, _, cx| this.toggle_vcs_menu(&ToggleVcsMenu, cx)),
), ),
)
.children(self.render_branches_popover_host()),
) )
} }
@ -503,131 +506,34 @@ impl CollabTitlebarItem {
.log_err(); .log_err();
} }
// pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) { fn render_branches_popover_host<'a>(&'a self) -> Option<AnyElement> {
// self.user_menu.update(cx, |user_menu, cx| { self.branch_popover.as_ref().map(|child| {
// let items = if let Some(_) = self.user_store.read(cx).current_user() { overlay()
// vec![ .child(div().min_w_64().child(child.clone()))
// ContextMenuItem::action("Settings", zed_actions::OpenSettings), .into_any()
// 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>( pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
// &'a self, if self.branch_popover.take().is_none() {
// _theme: &'a theme::Titlebar, if let Some(workspace) = self.workspace.upgrade() {
// cx: &'a mut ViewContext<Self>, let Some(view) = build_branch_list(workspace, cx).log_err() else {
// ) -> Option<AnyElement<Self>> { return;
// self.branch_popover.as_ref().map(|child| { };
// let theme = theme::current(cx).clone(); cx.subscribe(&view, |this, _, _, cx| {
// let child = ChildView::new(child, cx); this.branch_popover = None;
// let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| { cx.notify();
// child })
// .flex(1., true) .detach();
// .contained() self.project_popover.take();
// .constrained() let focus_handle = view.focus_handle(cx);
// .with_width(theme.titlebar.menu.width) cx.focus(&focus_handle);
// .with_height(theme.titlebar.menu.height) self.branch_popover = Some(view);
// }) }
// .on_click(MouseButton::Left, |_, _, _| {}) }
// .on_down_out(MouseButton::Left, move |_, this, cx| {
// this.branch_popover.take();
// cx.emit(());
// cx.notify();
// })
// .contained()
// .into_any();
// Overlay::new(child) cx.notify();
// .with_fit_mode(OverlayFitMode::SwitchAnchor) }
// .with_anchor_corner(AnchorCorner::TopLeft)
// .with_z_index(999)
// .aligned()
// .bottom()
// .left()
// .into_any()
// })
// }
// fn render_project_popover_host<'a>(
// &'a self,
// _theme: &'a theme::Titlebar,
// cx: &'a mut ViewContext<Self>,
// ) -> Option<AnyElement<Self>> {
// self.project_popover.as_ref().map(|child| {
// let theme = theme::current(cx).clone();
// let child = ChildView::new(child, cx);
// let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
// child
// .flex(1., true)
// .contained()
// .constrained()
// .with_width(theme.titlebar.menu.width)
// .with_height(theme.titlebar.menu.height)
// })
// .on_click(MouseButton::Left, |_, _, _| {})
// .on_down_out(MouseButton::Left, move |_, this, cx| {
// this.project_popover.take();
// cx.emit(());
// cx.notify();
// })
// .into_any();
// Overlay::new(child)
// .with_fit_mode(OverlayFitMode::SwitchAnchor)
// .with_anchor_corner(AnchorCorner::TopLeft)
// .with_z_index(999)
// .aligned()
// .bottom()
// .left()
// .into_any()
// })
// }
// pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
// if self.branch_popover.take().is_none() {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// let Some(view) =
// cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
// else {
// return;
// };
// cx.subscribe(&view, |this, _, event, cx| {
// match event {
// PickerEvent::Dismiss => {
// this.branch_popover = None;
// }
// }
// cx.notify();
// })
// .detach();
// self.project_popover.take();
// cx.focus(&view);
// self.branch_popover = Some(view);
// }
// }
// cx.notify();
// }
pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) { pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();

View file

@ -34,7 +34,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
ChatPanelSettings::register(cx); ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx); NotificationPanelSettings::register(cx);
// vcs_menu::init(cx); vcs_menu::init(cx);
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
collab_panel::init(cx); collab_panel::init(cx);
channel_view::init(cx); channel_view::init(cx);

View file

@ -1250,6 +1250,7 @@ impl CompletionsMenu {
let documentation_label = let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation { if let Some(Documentation::SingleLine(text)) = documentation {
Some(SharedString::from(text.clone())) Some(SharedString::from(text.clone()))
.filter(|text| !text.trim().is_empty())
} else { } else {
None None
}; };
@ -9762,19 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
.px_1p5() .px_1p5()
.child(HighlightedLabel::new(line.clone(), highlights.clone())) .child(HighlightedLabel::new(line.clone(), highlights.clone()))
.child( .child(
div() div().border().border_color(gpui::red()).child(
.border() IconButton::new(copy_id.clone(), Icon::Copy)
.border_color(gpui::red()) .icon_color(Color::Muted)
.invisible() .size(ButtonSize::Compact)
.group_hover(group_id, |style| style.visible()) .style(ButtonStyle::Transparent)
.child( .visible_on_hover(group_id)
IconButton::new(copy_id.clone(), Icon::Copy) .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
.icon_color(Color::Muted) .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
.size(ButtonSize::Compact) ),
.style(ButtonStyle::Transparent)
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
),
) )
})) }))
.into_any_element() .into_any_element()

View file

@ -23,13 +23,14 @@ use anyhow::Result;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use git::diff::DiffHunkStatus; use git::diff::DiffHunkStatus;
use gpui::{ use gpui::{
div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement, div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle, AnchorCorner, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds,
DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementId,
InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent, ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString,
Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
ViewContext, WeakView, WindowContext, WrappedLine,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::{language_settings::ShowWhitespaceSetting, Language}; use language::{language_settings::ShowWhitespaceSetting, Language};
@ -620,20 +621,8 @@ impl EditorElement {
let scroll_top = let scroll_top =
layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height; layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
let gutter_bg = cx.theme().colors().editor_gutter_background; let gutter_bg = cx.theme().colors().editor_gutter_background;
cx.paint_quad( cx.paint_quad(fill(gutter_bounds, gutter_bg));
gutter_bounds, cx.paint_quad(fill(text_bounds, self.style.background));
Corners::default(),
gutter_bg,
Edges::default(),
transparent_black(),
);
cx.paint_quad(
text_bounds,
Corners::default(),
self.style.background,
Edges::default(),
transparent_black(),
);
if let EditorMode::Full = layout.mode { if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable(); 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, layout.position_map.line_height * (end_row - start_row + 1) as f32,
); );
let active_line_bg = cx.theme().colors().editor_active_line_background; let active_line_bg = cx.theme().colors().editor_active_line_background;
cx.paint_quad( cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
Bounds { origin, size },
Corners::default(),
active_line_bg,
Edges::default(),
transparent_black(),
);
} }
} }
@ -679,13 +662,7 @@ impl EditorElement {
layout.position_map.line_height * highlighted_rows.len() as f32, layout.position_map.line_height * highlighted_rows.len() as f32,
); );
let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background; let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
cx.paint_quad( cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
Bounds { origin, size },
Corners::default(),
highlighted_line_bg,
Edges::default(),
transparent_black(),
);
} }
let scroll_left = let scroll_left =
@ -706,16 +683,13 @@ impl EditorElement {
} else { } else {
cx.theme().colors().editor_wrap_guide cx.theme().colors().editor_wrap_guide
}; };
cx.paint_quad( cx.paint_quad(fill(
Bounds { Bounds {
origin: point(x, text_bounds.origin.y), origin: point(x, text_bounds.origin.y),
size: size(px(1.), text_bounds.size.height), size: size(px(1.), text_bounds.size.height),
}, },
Corners::default(),
color, color,
Edges::default(), ));
transparent_black(),
);
} }
} }
} }
@ -812,13 +786,13 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y); let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y); let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size); let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad( cx.paint_quad(quad(
highlight_bounds, highlight_bounds,
Corners::all(1. * line_height), Corners::all(1. * line_height),
gpui::yellow(), // todo!("use the right color") gpui::yellow(), // todo!("use the right color")
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
); ));
continue; continue;
} }
@ -845,13 +819,13 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y); let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y); let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size); let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad( cx.paint_quad(quad(
highlight_bounds, highlight_bounds,
Corners::all(1. * line_height), Corners::all(1. * line_height),
cx.theme().status().deleted, cx.theme().status().deleted,
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
); ));
continue; continue;
} }
@ -867,13 +841,13 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y); let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y); let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size); let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad( cx.paint_quad(quad(
highlight_bounds, highlight_bounds,
Corners::all(0.05 * line_height), Corners::all(0.05 * line_height),
color, // todo!("use the right color") color, // todo!("use the right color")
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
); ));
} }
} }
@ -1120,123 +1094,120 @@ impl EditorElement {
cursor.paint(content_origin, cx); cursor.paint(content_origin, cx);
} }
}); });
cx.with_z_index(1, |cx| {
if let Some((position, mut context_menu)) = layout.context_menu.take() {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
let context_menu_size = context_menu.measure(available_space, cx);
let cursor_row_layout = &layout.position_map.line_layouts
[(position.row() - start_row) as usize]
.line;
let x = cursor_row_layout.x_for_index(position.column() as usize)
- layout.position_map.scroll_position.x;
let y = (position.row() + 1) as f32 * layout.position_map.line_height
- layout.position_map.scroll_position.y;
let mut list_origin = content_origin + point(x, y);
let list_width = context_menu_size.width;
let list_height = context_menu_size.height;
// Snap the right edge of the list to the right edge of the window if
// its horizontal bounds overflow.
if list_origin.x + list_width > cx.viewport_size().width {
list_origin.x =
(cx.viewport_size().width - list_width).max(Pixels::ZERO);
}
if list_origin.y + list_height > text_bounds.lower_right().y {
list_origin.y -= layout.position_map.line_height + list_height;
}
cx.break_content_mask(|cx| {
context_menu.draw(list_origin, available_space, cx)
});
}
if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
let available_space =
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
// This is safe because we check on layout whether the required row is available
let hovered_row_layout = &layout.position_map.line_layouts
[(position.row() - start_row) as usize]
.line;
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
// height. This is the size we will use to decide whether to render popovers above or below
// the hovered line.
let first_size = hover_popovers[0].measure(available_space, cx);
let height_to_reserve = first_size.height
+ 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
// Compute Hovered Point
let x = hovered_row_layout.x_for_index(position.column() as usize)
- layout.position_map.scroll_position.x;
let y = position.row() as f32 * layout.position_map.line_height
- layout.position_map.scroll_position.y;
let hovered_point = content_origin + point(x, y);
if hovered_point.y - height_to_reserve > Pixels::ZERO {
// There is enough space above. Render popovers above the hovered point
let mut current_y = hovered_point.y;
for mut hover_popover in hover_popovers {
let size = hover_popover.measure(available_space, cx);
let mut popover_origin =
point(hovered_point.x, current_y - size.height);
let x_out_of_bounds =
text_bounds.upper_right().x - (popover_origin.x + size.width);
if x_out_of_bounds < Pixels::ZERO {
popover_origin.x = popover_origin.x + x_out_of_bounds;
}
cx.break_content_mask(|cx| {
hover_popover.draw(popover_origin, available_space, cx)
});
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
let mut current_y = hovered_point.y + layout.position_map.line_height;
for mut hover_popover in hover_popovers {
let size = hover_popover.measure(available_space, cx);
let mut popover_origin = point(hovered_point.x, current_y);
let x_out_of_bounds =
text_bounds.upper_right().x - (popover_origin.x + size.width);
if x_out_of_bounds < Pixels::ZERO {
popover_origin.x = popover_origin.x + x_out_of_bounds;
}
hover_popover.draw(popover_origin, available_space, cx);
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
}
}
if let Some(mouse_context_menu) =
self.editor.read(cx).mouse_context_menu.as_ref()
{
let element = overlay()
.position(mouse_context_menu.position)
.child(mouse_context_menu.context_menu.clone())
.anchor(AnchorCorner::TopLeft)
.snap_to_window();
element.draw(
gpui::Point::default(),
size(AvailableSpace::MinContent, AvailableSpace::MinContent),
cx,
|_, _| {},
);
}
})
}, },
) )
} }
fn paint_overlays(
&mut self,
text_bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
let start_row = layout.visible_display_row_range.start;
if let Some((position, mut context_menu)) = layout.context_menu.take() {
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
let context_menu_size = context_menu.measure(available_space, cx);
let cursor_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
let x = cursor_row_layout.x_for_index(position.column() as usize)
- layout.position_map.scroll_position.x;
let y = (position.row() + 1) as f32 * layout.position_map.line_height
- layout.position_map.scroll_position.y;
let mut list_origin = content_origin + point(x, y);
let list_width = context_menu_size.width;
let list_height = context_menu_size.height;
// Snap the right edge of the list to the right edge of the window if
// its horizontal bounds overflow.
if list_origin.x + list_width > cx.viewport_size().width {
list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
}
if list_origin.y + list_height > text_bounds.lower_right().y {
list_origin.y -= layout.position_map.line_height + list_height;
}
cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx));
}
if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
// This is safe because we check on layout whether the required row is available
let hovered_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
// height. This is the size we will use to decide whether to render popovers above or below
// the hovered line.
let first_size = hover_popovers[0].measure(available_space, cx);
let height_to_reserve =
first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
// Compute Hovered Point
let x = hovered_row_layout.x_for_index(position.column() as usize)
- layout.position_map.scroll_position.x;
let y = position.row() as f32 * layout.position_map.line_height
- layout.position_map.scroll_position.y;
let hovered_point = content_origin + point(x, y);
if hovered_point.y - height_to_reserve > Pixels::ZERO {
// There is enough space above. Render popovers above the hovered point
let mut current_y = hovered_point.y;
for mut hover_popover in hover_popovers {
let size = hover_popover.measure(available_space, cx);
let mut popover_origin = point(hovered_point.x, current_y - size.height);
let x_out_of_bounds =
text_bounds.upper_right().x - (popover_origin.x + size.width);
if x_out_of_bounds < Pixels::ZERO {
popover_origin.x = popover_origin.x + x_out_of_bounds;
}
cx.break_content_mask(|cx| {
hover_popover.draw(popover_origin, available_space, cx)
});
current_y = popover_origin.y - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
let mut current_y = hovered_point.y + layout.position_map.line_height;
for mut hover_popover in hover_popovers {
let size = hover_popover.measure(available_space, cx);
let mut popover_origin = point(hovered_point.x, current_y);
let x_out_of_bounds =
text_bounds.upper_right().x - (popover_origin.x + size.width);
if x_out_of_bounds < Pixels::ZERO {
popover_origin.x = popover_origin.x + x_out_of_bounds;
}
hover_popover.draw(popover_origin, available_space, cx);
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
}
}
}
if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() {
let element = overlay()
.position(mouse_context_menu.position)
.child(mouse_context_menu.context_menu.clone())
.anchor(AnchorCorner::TopLeft)
.snap_to_window();
element.draw(
gpui::Point::default(),
size(AvailableSpace::MinContent, AvailableSpace::MinContent),
cx,
|_, _| {},
);
}
}
fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels { fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
bounds.upper_right().x - self.style.scrollbar_width bounds.upper_right().x - self.style.scrollbar_width
} }
@ -1278,7 +1249,7 @@ impl EditorElement {
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom)); let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
if layout.show_scrollbars { if layout.show_scrollbars {
cx.paint_quad( cx.paint_quad(quad(
track_bounds, track_bounds,
Corners::default(), Corners::default(),
cx.theme().colors().scrollbar_track_background, cx.theme().colors().scrollbar_track_background,
@ -1289,7 +1260,7 @@ impl EditorElement {
left: px(1.), left: px(1.),
}, },
cx.theme().colors().scrollbar_track_border, cx.theme().colors().scrollbar_track_border,
); ));
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
if layout.is_singleton && scrollbar_settings.selections { if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min(); let start_anchor = Anchor::min();
@ -1309,7 +1280,7 @@ impl EditorElement {
end_y = start_y + px(1.); end_y = start_y + px(1.);
} }
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y)); let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad( cx.paint_quad(quad(
bounds, bounds,
Corners::default(), Corners::default(),
cx.theme().status().info, cx.theme().status().info,
@ -1320,7 +1291,7 @@ impl EditorElement {
left: px(1.), left: px(1.),
}, },
cx.theme().colors().scrollbar_thumb_border, cx.theme().colors().scrollbar_thumb_border,
); ));
} }
} }
@ -1352,7 +1323,7 @@ impl EditorElement {
DiffHunkStatus::Modified => cx.theme().status().modified, DiffHunkStatus::Modified => cx.theme().status().modified,
DiffHunkStatus::Removed => cx.theme().status().deleted, DiffHunkStatus::Removed => cx.theme().status().deleted,
}; };
cx.paint_quad( cx.paint_quad(quad(
bounds, bounds,
Corners::default(), Corners::default(),
color, color,
@ -1363,11 +1334,11 @@ impl EditorElement {
left: px(1.), left: px(1.),
}, },
cx.theme().colors().scrollbar_thumb_border, cx.theme().colors().scrollbar_thumb_border,
); ));
} }
} }
cx.paint_quad( cx.paint_quad(quad(
thumb_bounds, thumb_bounds,
Corners::default(), Corners::default(),
cx.theme().colors().scrollbar_thumb_background, cx.theme().colors().scrollbar_thumb_background,
@ -1378,7 +1349,7 @@ impl EditorElement {
left: px(1.), left: px(1.),
}, },
cx.theme().colors().scrollbar_thumb_border, cx.theme().colors().scrollbar_thumb_border,
); ));
} }
let mouse_position = cx.mouse_position(); let mouse_position = cx.mouse_position();
@ -2833,32 +2804,30 @@ impl Element for EditorElement {
self.register_actions(cx); self.register_actions(cx);
self.register_key_listeners(cx); self.register_key_listeners(cx);
// We call with_z_index to establish a new stacking context. cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
cx.with_z_index(0, |cx| { let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
cx.with_content_mask(Some(ContentMask { bounds }), |cx| { cx.handle_input(&focus_handle, input_handler);
// 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);
self.paint_background(gutter_bounds, text_bounds, &layout, cx); self.paint_background(gutter_bounds, text_bounds, &layout, cx);
if layout.gutter_size.width > Pixels::ZERO { if layout.gutter_size.width > Pixels::ZERO {
self.paint_gutter(gutter_bounds, &mut layout, cx); self.paint_gutter(gutter_bounds, &mut layout, cx);
} }
self.paint_text(text_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() { if !layout.blocks.is_empty() {
cx.with_z_index(1, |cx| { cx.with_element_id(Some("editor_blocks"), |cx| {
cx.with_element_id(Some("editor_blocks"), |cx| { self.paint_blocks(bounds, &mut layout, 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 +3054,13 @@ impl Cursor {
}; };
//Draw background or border quad //Draw background or border quad
if matches!(self.shape, CursorShape::Hollow) { let cursor = if matches!(self.shape, CursorShape::Hollow) {
cx.paint_quad( outline(bounds, self.color)
bounds,
Corners::default(),
transparent_black(),
Edges::all(px(1.)),
self.color,
);
} else { } else {
cx.paint_quad( fill(bounds, self.color)
bounds, };
Corners::default(),
self.color, cx.paint_quad(cursor);
Edges::default(),
transparent_black(),
);
}
if let Some(block_text) = &self.block_text { if let Some(block_text) = &self.block_text {
block_text.paint(self.origin + origin, self.line_height, cx); block_text.paint(self.origin + origin, self.line_height, cx);

View file

@ -26,16 +26,17 @@ ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" } util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2"} workspace = { package = "workspace2", path = "../workspace2"}
log.workspace = true
futures.workspace = true
anyhow.workspace = true anyhow.workspace = true
smallvec.workspace = true futures.workspace = true
human_bytes = "0.4.1" human_bytes = "0.4.1"
isahc.workspace = true isahc.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true
postage.workspace = true postage.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
sysinfo.workspace = true sysinfo.workspace = true
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2" urlencoding = "2.1.2"

View file

@ -1,4 +1,4 @@
use std::{ops::RangeInclusive, sync::Arc}; use std::{ops::RangeInclusive, sync::Arc, time::Duration};
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
@ -6,8 +6,8 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
Model, PromptLevel, Render, Task, View, ViewContext, FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
}; };
use isahc::Request; use isahc::Request;
use language::Buffer; use language::Buffer;
@ -22,6 +22,7 @@ use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
// For UI testing purposes // For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true; 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 // Temporary, until tests are in place
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -47,13 +48,30 @@ struct FeedbackRequestBody<'a> {
token: &'a str, token: &'a str,
} }
#[derive(Debug, Clone, PartialEq)]
enum InvalidStateIssue {
EmailAddress,
CharacterCount,
}
#[derive(Debug, Clone, PartialEq)]
enum CannotSubmitReason {
InvalidState { issues: Vec<InvalidStateIssue> },
AwaitingSubmission,
}
#[derive(Debug, Clone, PartialEq)]
enum SubmissionState {
CanSubmit,
CannotSubmit { reason: CannotSubmitReason },
}
pub struct FeedbackModal { pub struct FeedbackModal {
system_specs: SystemSpecs, system_specs: SystemSpecs,
feedback_editor: View<Editor>, feedback_editor: View<Editor>,
email_address_editor: View<Editor>, email_address_editor: View<Editor>,
awaiting_submission: bool, submission_state: Option<SubmissionState>,
user_submitted: bool, dismiss_modal: bool,
discarded: bool,
character_count: i32, character_count: i32,
} }
@ -66,12 +84,7 @@ impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal { impl ModalView for FeedbackModal {
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool { fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.user_submitted { if self.dismiss_modal {
self.set_user_submitted(false, cx);
return true;
}
if self.discarded {
return true; return true;
} }
@ -85,7 +98,7 @@ impl ModalView for FeedbackModal {
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
if answer.await.ok() == Some(0) { if answer.await.ok() == Some(0) {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.discarded = true; this.dismiss_modal = true;
cx.emit(DismissEvent) cx.emit(DismissEvent)
}) })
.log_err(); .log_err();
@ -159,32 +172,27 @@ impl FeedbackModal {
editor editor
}); });
cx.subscribe( cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
&feedback_editor, if *event == EditorEvent::Edited {
|this, editor, event: &EditorEvent, cx| match event { this.character_count = editor
EditorEvent::Edited => { .read(cx)
this.character_count = editor .buffer()
.read(cx) .read(cx)
.buffer() .as_singleton()
.read(cx) .expect("Feedback editor is never a multi-buffer")
.as_singleton() .read(cx)
.expect("Feedback editor is never a multi-buffer") .len() as i32;
.read(cx) cx.notify();
.len() as i32; }
cx.notify(); })
}
_ => {}
},
)
.detach(); .detach();
Self { Self {
system_specs: system_specs.clone(), system_specs: system_specs.clone(),
feedback_editor, feedback_editor,
email_address_editor, email_address_editor,
awaiting_submission: false, submission_state: None,
user_submitted: false, dismiss_modal: false,
discarded: false,
character_count: 0, character_count: 0,
} }
} }
@ -205,19 +213,24 @@ impl FeedbackModal {
if answer == Some(0) { if answer == Some(0) {
match email.clone() { match email.clone() {
Some(email) => { Some(email) => {
let _ = KEY_VALUE_STORE KEY_VALUE_STORE
.write_kvp(DATABASE_KEY_NAME.to_string(), email) .write_kvp(DATABASE_KEY_NAME.to_string(), email)
.await; .await
.ok();
} }
None => { None => {
let _ = KEY_VALUE_STORE KEY_VALUE_STORE
.delete_kvp(DATABASE_KEY_NAME.to_string()) .delete_kvp(DATABASE_KEY_NAME.to_string())
.await; .await
.ok();
} }
}; };
this.update(&mut cx, |this, cx| { 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(); .log_err();
@ -227,7 +240,8 @@ impl FeedbackModal {
match res { match res {
Ok(_) => { Ok(_) => {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.set_user_submitted(true, cx); this.dismiss_modal = true;
cx.notify();
cx.emit(DismissEvent) cx.emit(DismissEvent)
}) })
.ok(); .ok();
@ -244,7 +258,9 @@ impl FeedbackModal {
prompt.await.ok(); prompt.await.ok();
}) })
.detach(); .detach();
this.set_awaiting_submission(false, cx);
this.submission_state = Some(SubmissionState::CanSubmit);
cx.notify();
}) })
.log_err(); .log_err();
} }
@ -256,16 +272,6 @@ impl FeedbackModal {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
fn set_awaiting_submission(&mut self, awaiting_submission: bool, cx: &mut ViewContext<Self>) {
self.awaiting_submission = awaiting_submission;
cx.notify();
}
fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext<Self>) {
self.user_submitted = user_submitted;
cx.notify();
}
async fn submit_feedback( async fn submit_feedback(
feedback_text: &str, feedback_text: &str,
email: Option<String>, email: Option<String>,
@ -273,6 +279,8 @@ impl FeedbackModal {
system_specs: SystemSpecs, system_specs: SystemSpecs,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if DEV_MODE { if DEV_MODE {
smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
if SEND_SUCCESS_IN_DEV_MODE { if SEND_SUCCESS_IN_DEV_MODE {
return Ok(()); return Ok(());
} else { } else {
@ -309,7 +317,67 @@ impl FeedbackModal {
Ok(()) Ok(())
} }
// TODO: Escape button calls dismiss fn update_submission_state(&mut self, cx: &mut ViewContext<Self>) {
if self.awaiting_submission() {
return;
}
let mut invalid_state_issues = Vec::new();
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_issues.push(InvalidStateIssue::EmailAddress);
}
if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
invalid_state_issues.push(InvalidStateIssue::CharacterCount);
}
if invalid_state_issues.is_empty() {
self.submission_state = Some(SubmissionState::CanSubmit);
} else {
self.submission_state = Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::InvalidState {
issues: invalid_state_issues,
},
});
}
}
fn valid_email_address(&self) -> bool {
!self.in_invalid_state(InvalidStateIssue::EmailAddress)
}
fn valid_character_count(&self) -> bool {
!self.in_invalid_state(InvalidStateIssue::CharacterCount)
}
fn in_invalid_state(&self, a: InvalidStateIssue) -> bool {
match self.submission_state {
Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::InvalidState { ref issues },
}) => issues.contains(&a),
_ => false,
}
}
fn awaiting_submission(&self) -> bool {
matches!(
self.submission_state,
Some(SubmissionState::CannotSubmit {
reason: CannotSubmitReason::AwaitingSubmission
})
)
}
fn can_submit(&self) -> bool {
matches!(self.submission_state, Some(SubmissionState::CanSubmit))
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent) cx.emit(DismissEvent)
} }
@ -319,17 +387,9 @@ impl Render for FeedbackModal {
type Element = Div; type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) { self.update_submission_state(cx);
Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
None => true,
};
let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count); let submit_button_text = if self.awaiting_submission() {
let allow_submission =
valid_character_count && valid_email_address && !self.awaiting_submission;
let submit_button_text = if self.awaiting_submission {
"Submitting..." "Submitting..."
} else { } else {
"Submit" "Submit"
@ -367,7 +427,7 @@ impl Render for FeedbackModal {
*FEEDBACK_CHAR_LIMIT.end() - self.character_count *FEEDBACK_CHAR_LIMIT.end() - self.character_count
) )
}) })
.color(if valid_character_count { .color(if self.valid_character_count() {
Color::Success Color::Success
} else { } else {
Color::Error Color::Error
@ -391,7 +451,11 @@ impl Render for FeedbackModal {
.p_2() .p_2()
.border() .border()
.rounded_md() .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(self.email_address_editor.clone()),
) )
.child( .child(
@ -424,11 +488,9 @@ impl Render for FeedbackModal {
})), })),
) )
.child( .child(
Button::new("send_feedback", submit_button_text) Button::new("submit_feedback", submit_button_text)
.color(Color::Accent) .color(Color::Accent)
.style(ButtonStyle::Filled) .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| { .on_click(cx.listener(|this, _, cx| {
this.submit(cx).detach(); this.submit(cx).detach();
})) }))
@ -440,7 +502,7 @@ impl Render for FeedbackModal {
cx, cx,
) )
}) })
.when(!allow_submission, |this| this.disabled(true)), .when(!self.can_submit(), |this| this.disabled(true)),
), ),
), ),
), ),
@ -450,3 +512,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: 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. // 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));
// });
// })?;
// }
// }

View file

@ -1138,6 +1138,10 @@ impl AppContext {
pub fn has_active_drag(&self) -> bool { pub fn has_active_drag(&self) -> bool {
self.active_drag.is_some() self.active_drag.is_some()
} }
pub fn active_drag(&self) -> Option<AnyView> {
self.active_drag.as_ref().map(|drag| drag.view.clone())
}
} }
impl Context for AppContext { impl Context for AppContext {

View file

@ -482,48 +482,31 @@ impl IntoElement for AnyElement {
} }
} }
// impl<V, E, F> Element for Option<F> /// The empty element, which renders nothing.
// where pub type Empty = ();
// V: 'static,
// E: Element,
// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
// {
// type State = Option<AnyElement>;
// fn element_id(&self) -> Option<ElementId> { impl IntoElement for () {
// None type Element = Self;
// }
// fn layout( fn element_id(&self) -> Option<ElementId> {
// &mut self, None
// _: Option<Self::State>, }
// cx: &mut WindowContext,
// ) -> (LayoutId, Self::State) {
// let render = self.take().unwrap();
// let mut element = (render)(view_state, cx).into_any();
// let layout_id = element.layout(view_state, cx);
// (layout_id, Some(element))
// }
// fn paint( fn into_element(self) -> Self::Element {
// self, self
// _bounds: Bounds<Pixels>, }
// rendered_element: &mut Self::State, }
// cx: &mut WindowContext,
// ) {
// rendered_element.take().unwrap().paint(view_state, cx);
// }
// }
// impl<V, E, F> RenderOnce for Option<F> impl Element for () {
// where type State = ();
// V: 'static,
// E: Element,
// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
// {
// type Element = Self;
// fn render(self) -> Self::Element { fn layout(
// self &mut self,
// } _state: Option<Self::State>,
// } cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
(cx.request_layout(&crate::Style::default(), None), ())
}
fn paint(self, _bounds: Bounds<Pixels>, _state: &mut Self::State, _cx: &mut WindowContext) {}
}

View file

@ -2,7 +2,7 @@ use refineable::Refineable as _;
use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas { pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
Canvas { Canvas {
paint_callback: Box::new(callback), paint_callback: Box::new(callback),
style: StyleRefinement::default(), style: StyleRefinement::default(),
@ -10,7 +10,7 @@ pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext
} }
pub struct Canvas { pub struct Canvas {
paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>, paint_callback: Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>,
style: StyleRefinement, style: StyleRefinement,
} }
@ -41,7 +41,7 @@ impl Element for Canvas {
} }
fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) { fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
(self.paint_callback)(bounds, cx) (self.paint_callback)(&bounds, cx)
} }
} }

View file

@ -6,6 +6,7 @@ use crate::{
SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
WindowContext, WindowContext,
}; };
use collections::HashMap; use collections::HashMap;
use refineable::Refineable; use refineable::Refineable;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -29,6 +30,11 @@ pub struct GroupStyle {
pub style: Box<StyleRefinement>, pub style: Box<StyleRefinement>,
} }
pub struct DragMoveEvent<W: Render> {
pub event: MouseMoveEvent,
pub drag: View<W>,
}
pub trait InteractiveElement: Sized { pub trait InteractiveElement: Sized {
fn interactivity(&mut self) -> &mut Interactivity; fn interactivity(&mut self) -> &mut Interactivity;
@ -192,6 +198,34 @@ pub trait InteractiveElement: Sized {
self self
} }
fn on_drag_move<W>(
mut self,
listener: impl Fn(&DragMoveEvent<W>, &mut WindowContext) + 'static,
) -> Self
where
W: Render,
{
self.interactivity().mouse_move_listeners.push(Box::new(
move |event, bounds, phase, cx| {
if phase == DispatchPhase::Capture
&& bounds.drag_target_contains(&event.position, cx)
{
if let Some(view) = cx.active_drag().and_then(|view| view.downcast::<W>().ok())
{
(listener)(
&DragMoveEvent {
event: event.clone(),
drag: view,
},
cx,
);
}
}
},
));
self
}
fn on_scroll_wheel( fn on_scroll_wheel(
mut self, mut self,
listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
@ -403,7 +437,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self self
} }
fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self fn on_drag<W>(mut self, constructor: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
where where
Self: Sized, Self: Sized,
W: 'static + Render, W: 'static + Render,
@ -413,7 +447,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
"calling on_drag more than once on the same element is not supported" "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 { self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
view: listener(cx).into(), view: constructor(cx).into(),
cursor_offset, cursor_offset,
})); }));
self self
@ -493,11 +527,19 @@ pub type DragEventListener = Box<dyn Fn(&MouseMoveEvent, &mut WindowContext) + '
pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>; pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
#[track_caller]
pub fn div() -> Div { pub fn div() -> Div {
Div { let mut div = Div {
interactivity: Interactivity::default(), interactivity: Interactivity::default(),
children: SmallVec::default(), children: SmallVec::default(),
};
#[cfg(debug_assertions)]
{
div.interactivity.location = Some(*core::panic::Location::caller());
} }
div
} }
pub struct Div { pub struct Div {
@ -600,17 +642,10 @@ impl Element for Div {
&mut element_state.interactive_state, &mut element_state.interactive_state,
cx, cx,
|style, scroll_offset, cx| { |style, scroll_offset, cx| {
if style.visibility == Visibility::Hidden {
return;
}
let z_index = style.z_index.unwrap_or(0); let z_index = style.z_index.unwrap_or(0);
cx.with_z_index(z_index, |cx| { cx.with_z_index(z_index, |cx| {
cx.with_z_index(0, |cx| { style.paint(bounds, cx, |cx| {
style.paint(bounds, cx);
});
cx.with_z_index(1, |cx| {
cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_content_mask(style.overflow_mask(bounds), |cx| {
cx.with_element_offset(scroll_offset, |cx| { cx.with_element_offset(scroll_offset, |cx| {
@ -620,7 +655,7 @@ impl Element for Div {
}) })
}) })
}) })
}) });
}) })
}, },
); );
@ -681,6 +716,9 @@ pub struct Interactivity {
pub drag_listener: Option<DragListener>, pub drag_listener: Option<DragListener>,
pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>, pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
pub tooltip_builder: Option<TooltipBuilder>, pub tooltip_builder: Option<TooltipBuilder>,
#[cfg(debug_assertions)]
pub location: Option<core::panic::Location<'static>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -740,6 +778,121 @@ impl Interactivity {
) { ) {
let style = self.compute_style(Some(bounds), element_state, cx); 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::<crate::DebugBelow>())
&& bounds.contains(&cx.mouse_position())
{
const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
let element_id = format!("{:?}", self.element_id.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 if style
.background .background
.as_ref() .as_ref()
@ -1152,81 +1305,87 @@ impl Interactivity {
let mut style = Style::default(); let mut style = Style::default();
style.refine(&self.base_style); style.refine(&self.base_style);
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
if let Some(in_focus_style) = self.in_focus_style.as_ref() { if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
if focus_handle.within_focused(cx) { if let Some(in_focus_style) = self.in_focus_style.as_ref() {
style.refine(in_focus_style); if focus_handle.within_focused(cx) {
style.refine(in_focus_style);
}
} }
}
if let Some(focus_style) = self.focus_style.as_ref() { if let Some(focus_style) = self.focus_style.as_ref() {
if focus_handle.is_focused(cx) { if focus_handle.is_focused(cx) {
style.refine(focus_style); 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(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() { if let Some(bounds) = bounds {
for (state_type, group_drag_style) in &self.group_drag_over_styles { let mouse_position = cx.mouse_position();
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { if let Some(group_hover) = self.group_hover_style.as_ref() {
if *state_type == drag.view.entity_type() if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
&& group_bounds.contains(&mouse_position) 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);
} }
} }
} }
if let Some(hover_style) = self.hover_style.as_ref() {
for (state_type, drag_over_style) in &self.drag_over_styles { if bounds
if *state_type == drag.view.entity_type() .intersect(&cx.content_mask().bounds)
&& bounds .contains(&mouse_position)
.intersect(&cx.content_mask().bounds) && cx.was_top_layer(&mouse_position, cx.stacking_order())
.contains(&mouse_position)
{ {
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.view.entity_type()
&& group_bounds.contains(&mouse_position)
{
style.refine(&group_drag_style.style);
}
}
}
let clicked_state = element_state for (state_type, drag_over_style) in &self.drag_over_styles {
.clicked_state if *state_type == drag.view.entity_type()
.get_or_insert_with(Default::default) && bounds
.borrow(); .intersect(&cx.content_mask().bounds)
if clicked_state.group { .contains(&mouse_position)
if let Some(group) = self.group_active_style.as_ref() { && cx.was_top_layer_under_active_drag(
style.refine(&group.style) &mouse_position,
} cx.stacking_order(),
} )
{
style.refine(drag_over_style);
}
}
if let Some(active_style) = self.active_style.as_ref() { cx.active_drag = Some(drag);
if clicked_state.element { }
style.refine(active_style)
} }
}
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 style
} }
@ -1263,6 +1422,9 @@ impl Default for Interactivity {
drag_listener: None, drag_listener: None,
hover_listener: None, hover_listener: None,
tooltip_builder: None, tooltip_builder: None,
#[cfg(debug_assertions)]
location: None,
} }
} }
} }

View file

@ -10,6 +10,7 @@ use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height. /// 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, /// 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. /// uniform_list will only render the visible subset of items.
#[track_caller]
pub fn uniform_list<I, R, V>( pub fn uniform_list<I, R, V>(
view: View<V>, view: View<V>,
id: I, id: I,
@ -42,6 +43,10 @@ where
interactivity: Interactivity { interactivity: Interactivity {
element_id: Some(id.into()), element_id: Some(id.into()),
base_style: Box::new(base_style), base_style: Box::new(base_style),
#[cfg(debug_assertions)]
location: Some(*core::panic::Location::caller()),
..Default::default() ..Default::default()
}, },
scroll_handle: None, scroll_handle: None,
@ -197,41 +202,41 @@ impl Element for UniformList {
); );
cx.with_z_index(style.z_index.unwrap_or(0), |cx| { 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 { let first_visible_element_ix =
if let Some(scroll_handle) = self.scroll_handle.clone() { (-scroll_offset.y / item_height).floor() as usize;
scroll_handle.0.borrow_mut().replace(ScrollHandleState { let last_visible_element_ix =
item_height, ((-scroll_offset.y + padded_bounds.size.height) / item_height)
list_height: padded_bounds.size.height, .ceil() as usize;
scroll_offset: shared_scroll_offset, 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);
}
});
}); });
} }
});
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);
}
});
});
}
}) })
}, },
); );

View file

@ -1592,6 +1592,17 @@ impl Edges<Pixels> {
} }
} }
impl Into<Edges<Pixels>> for f32 {
fn into(self) -> Edges<Pixels> {
Edges {
top: self.into(),
right: self.into(),
bottom: self.into(),
left: self.into(),
}
}
}
/// Represents the corners of a box in a 2D space, such as border radius. /// 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`. /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
@ -1808,6 +1819,28 @@ where
impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {} impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
impl Into<Corners<Pixels>> for f32 {
fn into(self) -> Corners<Pixels> {
Corners {
top_left: self.into(),
top_right: self.into(),
bottom_right: self.into(),
bottom_left: self.into(),
}
}
}
impl Into<Corners<Pixels>> for Pixels {
fn into(self) -> Corners<Pixels> {
Corners {
top_left: self,
top_right: self,
bottom_right: self,
bottom_left: self,
}
}
}
/// Represents a length in pixels, the base unit of measurement in the UI framework. /// 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 /// `Pixels` is a value type that represents an absolute length in pixels, which is used

View file

@ -147,6 +147,7 @@ pub trait PlatformWindow {
fn appearance(&self) -> WindowAppearance; fn appearance(&self) -> WindowAppearance;
fn display(&self) -> Rc<dyn PlatformDisplay>; fn display(&self) -> Rc<dyn PlatformDisplay>;
fn mouse_position(&self) -> Point<Pixels>; fn mouse_position(&self) -> Point<Pixels>;
fn modifiers(&self) -> Modifiers;
fn as_any_mut(&mut self) -> &mut dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>); fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
fn clear_input_handler(&mut self); fn clear_input_handler(&mut self);

View file

@ -9,9 +9,10 @@ use crate::{
use block::ConcreteBlock; use block::ConcreteBlock;
use cocoa::{ use cocoa::{
appkit::{ appkit::{
CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard, CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowStyleMask, NSWindowTitleVisibility,
}, },
base::{id, nil}, base::{id, nil},
foundation::{ foundation::{
@ -744,6 +745,26 @@ impl PlatformWindow for MacWindow {
convert_mouse_position(position, self.content_size().height) 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 { fn as_any_mut(&mut self) -> &mut dyn Any {
self self
} }

View file

@ -79,6 +79,10 @@ impl PlatformWindow for TestWindow {
Point::default() Point::default()
} }
fn modifiers(&self) -> crate::Modifiers {
crate::Modifiers::default()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self self
} }

View file

@ -54,6 +54,7 @@ impl SceneBuilder {
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32; layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
} }
self.layers_by_order.clear(); self.layers_by_order.clear();
self.last_order = None;
// Add all primitives to the BSP splitter to determine draw order // Add all primitives to the BSP splitter to determine draw order
self.splitter.reset(); self.splitter.reset();

View file

@ -1,9 +1,9 @@
use std::{iter, mem, ops::Range}; use std::{iter, mem, ops::Range};
use crate::{ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
}; };
use collections::HashSet; use collections::HashSet;
@ -14,6 +14,9 @@ pub use taffy::style::{
Overflow, Position, Overflow, Position,
}; };
#[cfg(debug_assertions)]
pub struct DebugBelow;
pub type StyleCascade = Cascade<Style>; pub type StyleCascade = Cascade<Style>;
#[derive(Clone, Refineable, Debug)] #[derive(Clone, Refineable, Debug)]
@ -108,6 +111,11 @@ pub struct Style {
pub mouse_cursor: Option<CursorStyle>, pub mouse_cursor: Option<CursorStyle>,
pub z_index: Option<u8>, pub z_index: Option<u8>,
#[cfg(debug_assertions)]
pub debug: bool,
#[cfg(debug_assertions)]
pub debug_below: bool,
} }
impl Styled for StyleRefinement { impl Styled for StyleRefinement {
@ -334,7 +342,22 @@ impl Style {
} }
/// Paints the background of an element styled with this style. /// Paints the background of an element styled with this style.
pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) { pub fn paint(
&self,
bounds: Bounds<Pixels>,
cx: &mut WindowContext,
continuation: impl FnOnce(&mut WindowContext),
) {
#[cfg(debug_assertions)]
if self.debug_below {
cx.set_global(DebugBelow)
}
#[cfg(debug_assertions)]
if self.debug || cx.has_global::<DebugBelow>() {
cx.paint_quad(crate::outline(bounds, crate::red()));
}
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
cx.with_z_index(0, |cx| { cx.with_z_index(0, |cx| {
@ -348,15 +371,24 @@ impl Style {
let background_color = self.background.as_ref().and_then(Fill::color); let background_color = self.background.as_ref().and_then(Fill::color);
if background_color.is_some() || self.is_border_visible() { if background_color.is_some() || self.is_border_visible() {
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
cx.paint_quad( cx.paint_quad(quad(
bounds, bounds,
self.corner_radii.to_pixels(bounds.size, rem_size), self.corner_radii.to_pixels(bounds.size, rem_size),
background_color.unwrap_or_default(), background_color.unwrap_or_default(),
self.border_widths.to_pixels(rem_size), self.border_widths.to_pixels(rem_size),
self.border_color.unwrap_or_default(), self.border_color.unwrap_or_default(),
); ));
}); });
} }
cx.with_z_index(2, |cx| {
continuation(cx);
});
#[cfg(debug_assertions)]
if self.debug_below {
cx.remove_global::<DebugBelow>();
}
} }
fn is_border_visible(&self) -> bool { fn is_border_visible(&self) -> bool {
@ -404,6 +436,11 @@ impl Default for Style {
text: TextStyleRefinement::default(), text: TextStyleRefinement::default(),
mouse_cursor: None, mouse_cursor: None,
z_index: None, z_index: None,
#[cfg(debug_assertions)]
debug: false,
#[cfg(debug_assertions)]
debug_below: false,
} }
} }
} }

View file

@ -633,4 +633,16 @@ pub trait Styled: Sized {
.line_height = Some(line_height.into()); .line_height = Some(line_height.into());
self self
} }
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {
self.style().debug = Some(true);
self
}
#[cfg(debug_assertions)]
fn debug_below(mut self) -> Self {
self.style().debug_below = Some(true);
self
}
} }

View file

@ -1,7 +1,6 @@
use crate::{ use crate::{
black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
WrappedLineLayout,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -109,16 +108,13 @@ fn paint_line(
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next(); wraps.next();
if let Some((background_origin, background_color)) = current_background.as_mut() { if let Some((background_origin, background_color)) = current_background.as_mut() {
cx.paint_quad( cx.paint_quad(fill(
Bounds { Bounds {
origin: *background_origin, origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height), size: size(glyph_origin.x - background_origin.x, line_height),
}, },
Corners::default(),
*background_color, *background_color,
Edges::default(), ));
transparent_black(),
);
background_origin.x = origin.x; background_origin.x = origin.x;
background_origin.y += line_height; background_origin.y += line_height;
} }
@ -180,16 +176,13 @@ fn paint_line(
} }
if let Some((background_origin, background_color)) = finished_background { if let Some((background_origin, background_color)) = finished_background {
cx.paint_quad( cx.paint_quad(fill(
Bounds { Bounds {
origin: background_origin, origin: background_origin,
size: size(glyph_origin.x - background_origin.x, line_height), size: size(glyph_origin.x - background_origin.x, line_height),
}, },
Corners::default(),
background_color, background_color,
Edges::default(), ));
transparent_black(),
);
} }
if let Some((underline_origin, underline_style)) = finished_underline { if let Some((underline_origin, underline_style)) = finished_underline {
@ -235,16 +228,13 @@ fn paint_line(
} }
if let Some((background_origin, background_color)) = current_background.take() { if let Some((background_origin, background_color)) = current_background.take() {
cx.paint_quad( cx.paint_quad(fill(
Bounds { Bounds {
origin: background_origin, origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height), size: size(last_line_end_x - background_origin.x, line_height),
}, },
Corners::default(),
background_color, background_color,
Edges::default(), ));
transparent_black(),
);
} }
if let Some((underline_start, underline_style)) = current_underline.take() { if let Some((underline_start, underline_style)) = current_underline.take() {

View file

@ -1,15 +1,15 @@
use crate::{ use crate::{
key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext, key_dispatch::DispatchActionListener, px, size, transparent_black, Action, AnyDrag, AnyView,
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId,
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId, Hsla, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent,
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString,
SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::FxHashMap; use collections::FxHashMap;
@ -243,6 +243,7 @@ pub struct Window {
pub(crate) blur_listeners: SubscriberSet<(), AnyObserver>, pub(crate) blur_listeners: SubscriberSet<(), AnyObserver>,
default_prevented: bool, default_prevented: bool,
mouse_position: Point<Pixels>, mouse_position: Point<Pixels>,
modifiers: Modifiers,
requested_cursor_style: Option<CursorStyle>, requested_cursor_style: Option<CursorStyle>,
scale_factor: f32, scale_factor: f32,
bounds: WindowBounds, bounds: WindowBounds,
@ -315,6 +316,7 @@ impl Window {
let display_id = platform_window.display().id(); let display_id = platform_window.display().id();
let sprite_atlas = platform_window.sprite_atlas(); let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position(); let mouse_position = platform_window.mouse_position();
let modifiers = platform_window.modifiers();
let content_size = platform_window.content_size(); let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor(); let scale_factor = platform_window.scale_factor();
let bounds = platform_window.bounds(); let bounds = platform_window.bounds();
@ -378,6 +380,7 @@ impl Window {
blur_listeners: SubscriberSet::new(), blur_listeners: SubscriberSet::new(),
default_prevented: true, default_prevented: true,
mouse_position, mouse_position,
modifiers,
requested_cursor_style: None, requested_cursor_style: None,
scale_factor, scale_factor,
bounds, bounds,
@ -893,6 +896,11 @@ impl<'a> WindowContext<'a> {
self.window.mouse_position self.window.mouse_position
} }
/// The current state of the keyboard's modifiers
pub fn modifiers(&self) -> Modifiers {
self.window.modifiers
}
pub fn set_cursor_style(&mut self, style: CursorStyle) { pub fn set_cursor_style(&mut self, style: CursorStyle) {
self.window.requested_cursor_style = Some(style) self.window.requested_cursor_style = Some(style)
} }
@ -979,14 +987,8 @@ impl<'a> WindowContext<'a> {
/// Paint one or more quads into the scene for the next frame at the current stacking context. /// Paint one or more quads into the scene for the next frame at the current stacking context.
/// Quads are colored rectangular regions with an optional background, border, and corner radius. /// Quads are colored rectangular regions with an optional background, border, and corner radius.
pub fn paint_quad( /// see [`fill`], [`outline`], and [`quad`] to construct this type.
&mut self, pub fn paint_quad(&mut self, quad: PaintQuad) {
bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
background: impl Into<Hsla>,
border_widths: Edges<Pixels>,
border_color: impl Into<Hsla>,
) {
let scale_factor = self.scale_factor(); let scale_factor = self.scale_factor();
let content_mask = self.content_mask(); let content_mask = self.content_mask();
@ -995,12 +997,12 @@ impl<'a> WindowContext<'a> {
&window.next_frame.z_index_stack, &window.next_frame.z_index_stack,
Quad { Quad {
order: 0, order: 0,
bounds: bounds.scale(scale_factor), bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor), content_mask: content_mask.scale(scale_factor),
background: background.into(), background: quad.background,
border_color: border_color.into(), border_color: quad.border_color,
corner_radii: corner_radii.scale(scale_factor), corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: border_widths.scale(scale_factor), border_widths: quad.border_widths.scale(scale_factor),
}, },
); );
} }
@ -1363,16 +1365,34 @@ impl<'a> WindowContext<'a> {
// API for the mouse position can only occur on the main thread. // API for the mouse position can only occur on the main thread.
InputEvent::MouseMove(mouse_move) => { InputEvent::MouseMove(mouse_move) => {
self.window.mouse_position = mouse_move.position; self.window.mouse_position = mouse_move.position;
self.window.modifiers = mouse_move.modifiers;
InputEvent::MouseMove(mouse_move) InputEvent::MouseMove(mouse_move)
} }
InputEvent::MouseDown(mouse_down) => { InputEvent::MouseDown(mouse_down) => {
self.window.mouse_position = mouse_down.position; self.window.mouse_position = mouse_down.position;
self.window.modifiers = mouse_down.modifiers;
InputEvent::MouseDown(mouse_down) InputEvent::MouseDown(mouse_down)
} }
InputEvent::MouseUp(mouse_up) => { InputEvent::MouseUp(mouse_up) => {
self.window.mouse_position = mouse_up.position; self.window.mouse_position = mouse_up.position;
self.window.modifiers = mouse_up.modifiers;
InputEvent::MouseUp(mouse_up) InputEvent::MouseUp(mouse_up)
} }
InputEvent::MouseExited(mouse_exited) => {
// todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?")
self.window.modifiers = mouse_exited.modifiers;
InputEvent::MouseExited(mouse_exited)
}
InputEvent::ModifiersChanged(modifiers_changed) => {
self.window.modifiers = modifiers_changed.modifiers;
InputEvent::ModifiersChanged(modifiers_changed)
}
InputEvent::ScrollWheel(scroll_wheel) => {
self.window.mouse_position = scroll_wheel.position;
self.window.modifiers = scroll_wheel.modifiers;
InputEvent::ScrollWheel(scroll_wheel)
}
// Translate dragging and dropping of external files from the operating system // Translate dragging and dropping of external files from the operating system
// to internal drag and drop events. // to internal drag and drop events.
InputEvent::FileDrop(file_drop) => match file_drop { InputEvent::FileDrop(file_drop) => match file_drop {
@ -1415,7 +1435,7 @@ impl<'a> WindowContext<'a> {
click_count: 1, click_count: 1,
}), }),
}, },
_ => event, InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
}; };
if let Some(any_mouse_event) = event.mouse_event() { if let Some(any_mouse_event) = event.mouse_event() {
@ -2985,3 +3005,85 @@ impl From<(&'static str, u64)> for ElementId {
ElementId::NamedInteger(name.into(), id as usize) ElementId::NamedInteger(name.into(), id as usize)
} }
} }
/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
pub struct PaintQuad {
bounds: Bounds<Pixels>,
corner_radii: Corners<Pixels>,
background: Hsla,
border_widths: Edges<Pixels>,
border_color: Hsla,
}
impl PaintQuad {
/// Set the corner radii of the quad.
pub fn corner_radii(self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
PaintQuad {
corner_radii: corner_radii.into(),
..self
}
}
/// Set the border widths of the quad.
pub fn border_widths(self, border_widths: impl Into<Edges<Pixels>>) -> Self {
PaintQuad {
border_widths: border_widths.into(),
..self
}
}
/// Set the border color of the quad.
pub fn border_color(self, border_color: impl Into<Hsla>) -> Self {
PaintQuad {
border_color: border_color.into(),
..self
}
}
/// Set the background color of the quad.
pub fn background(self, background: impl Into<Hsla>) -> Self {
PaintQuad {
background: background.into(),
..self
}
}
}
/// Create a quad with the given parameters.
pub fn quad(
bounds: Bounds<Pixels>,
corner_radii: impl Into<Corners<Pixels>>,
background: impl Into<Hsla>,
border_widths: impl Into<Edges<Pixels>>,
border_color: impl Into<Hsla>,
) -> PaintQuad {
PaintQuad {
bounds,
corner_radii: corner_radii.into(),
background: background.into(),
border_widths: border_widths.into(),
border_color: border_color.into(),
}
}
/// Create a filled quad with the given bounds and background color.
pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
PaintQuad {
bounds: bounds.into(),
corner_radii: (0.).into(),
background: background.into(),
border_widths: (0.).into(),
border_color: transparent_black(),
}
}
/// Create a rectangle outline with the given bounds, border color, and a 1px border width
pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>) -> PaintQuad {
PaintQuad {
bounds: bounds.into(),
corner_radii: (0.).into(),
background: transparent_black(),
border_widths: (1.).into(),
border_color: border_color.into(),
}
}

View file

@ -0,0 +1,23 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
pub fn derive_render(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let gen = quote! {
impl #impl_generics gpui::Render for #type_name #type_generics
#where_clause
{
type Element = ();
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
()
}
}
};
gen.into()
}

View file

@ -1,4 +1,5 @@
mod derive_into_element; mod derive_into_element;
mod derive_render;
mod register_action; mod register_action;
mod style_helpers; mod style_helpers;
mod test; mod test;
@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
derive_into_element::derive_into_element(input) derive_into_element::derive_into_element(input)
} }
#[proc_macro_derive(Render)]
pub fn derive_render(input: TokenStream) -> TokenStream {
derive_render::derive_render(input)
}
#[proc_macro] #[proc_macro]
pub fn style_helpers(input: TokenStream) -> TokenStream { pub fn style_helpers(input: TokenStream) -> TokenStream {
style_helpers::style_helpers(input) style_helpers::style_helpers(input)

View file

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50",
"version": "1.0.12" "version": "1.1.4"
} }
}, },
{ {
@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08",
"version": "104.5112.17" "version": "114.5735.8"
} }
}, },
{ {

View file

@ -15,7 +15,7 @@ let package = Package(
targets: ["LiveKitBridge"]), targets: ["LiveKitBridge"]),
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.

View file

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50",
"version": "1.0.12" "version": "1.1.4"
} }
}, },
{ {
@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08",
"version": "104.5112.17" "version": "114.5735.8"
} }
}, },
{ {

View file

@ -15,7 +15,7 @@ let package = Package(
targets: ["LiveKitBridge2"]), targets: ["LiveKitBridge2"]),
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.

View file

@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" } util = { path = "../util" }
theme = { package = "theme2", path = "../theme2" } theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2"}
parking_lot.workspace = true parking_lot.workspace = true

View file

@ -1,11 +1,12 @@
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter, div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task,
View, ViewContext, WindowContext, UniformListScrollHandle, View, ViewContext, WindowContext,
}; };
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Color, Divider, Label}; use ui::{prelude::*, v_stack, Color, Divider, Label};
use workspace::ModalView;
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
@ -13,6 +14,7 @@ pub struct Picker<D: PickerDelegate> {
editor: View<Editor>, editor: View<Editor>,
pending_update_matches: Option<Task<()>>, pending_update_matches: Option<Task<()>>,
confirm_on_update: Option<bool>, confirm_on_update: Option<bool>,
width: Option<Length>,
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
@ -55,11 +57,17 @@ impl<D: PickerDelegate> Picker<D> {
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
pending_update_matches: None, pending_update_matches: None,
confirm_on_update: None, confirm_on_update: None,
width: None,
}; };
this.update_matches("".to_string(), cx); this.update_matches("".to_string(), cx);
this this
} }
pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
self.width = Some(width.into());
self
}
pub fn focus(&self, cx: &mut WindowContext) { pub fn focus(&self, cx: &mut WindowContext) {
self.editor.update(cx, |editor, cx| editor.focus(cx)); self.editor.update(cx, |editor, cx| editor.focus(cx));
} }
@ -197,6 +205,7 @@ impl<D: PickerDelegate> Picker<D> {
} }
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {} impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
impl<D: PickerDelegate> ModalView for Picker<D> {}
impl<D: PickerDelegate> Render for Picker<D> { impl<D: PickerDelegate> Render for Picker<D> {
type Element = Div; type Element = Div;
@ -221,6 +230,9 @@ impl<D: PickerDelegate> Render for Picker<D> {
div() div()
.key_context("picker") .key_context("picker")
.size_full() .size_full()
.when_some(self.width, |el, width| {
el.w(width)
})
.overflow_hidden() .overflow_hidden()
.elevation_3(cx) .elevation_3(cx)
.on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_next))
@ -271,7 +283,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
}, },
) )
.track_scroll(self.scroll_handle.clone()) .track_scroll(self.scroll_handle.clone())
.p_1()
) )
.max_h_72() .max_h_72()
.overflow_hidden(), .overflow_hidden(),

View file

@ -0,0 +1,37 @@
[package]
name = "project_symbols2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/project_symbols.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
gpui = {package = "gpui2", path = "../gpui2" }
picker = {package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
text = {package = "text2", path = "../text2" }
settings = {package = "settings2", path = "../settings2" }
workspace = {package = "workspace2", path = "../workspace2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
anyhow.workspace = true
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
[dev-dependencies]
futures.workspace = true
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

View file

@ -0,0 +1,411 @@
use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task,
View, ViewContext, WeakView,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::{Project, Symbol};
use std::{borrow::Cow, cmp::Reverse, sync::Arc};
use theme::ActiveTheme;
use util::ResultExt;
use workspace::{
ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable},
Workspace,
};
actions!(project_symbols, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &Toggle, cx| {
let project = workspace.project().clone();
let handle = cx.view().downgrade();
workspace.toggle_modal(cx, move |cx| {
let delegate = ProjectSymbolsDelegate::new(handle, project);
Picker::new(delegate, cx).width(rems(34.))
})
});
},
)
.detach();
}
pub type ProjectSymbols = View<Picker<ProjectSymbolsDelegate>>;
pub struct ProjectSymbolsDelegate {
workspace: WeakView<Workspace>,
project: Model<Project>,
selected_match_index: usize,
symbols: Vec<Symbol>,
visible_match_candidates: Vec<StringMatchCandidate>,
external_match_candidates: Vec<StringMatchCandidate>,
show_worktree_root_name: bool,
matches: Vec<StringMatch>,
}
impl ProjectSymbolsDelegate {
fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self {
workspace,
project,
selected_match_index: 0,
symbols: Default::default(),
visible_match_candidates: Default::default(),
external_match_candidates: Default::default(),
matches: Default::default(),
show_worktree_root_name: false,
}
}
fn filter(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) {
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&self.visible_match_candidates,
query,
false,
MAX_MATCHES,
&Default::default(),
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&self.external_match_candidates,
query,
false,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&Default::default(),
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &self.symbols[mat.candidate_id];
(
Reverse(OrderedFloat(mat.score)),
&symbol.label.text[symbol.label.filter_range.clone()],
)
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
for mat in &mut matches {
let symbol = &self.symbols[mat.candidate_id];
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
}
self.matches = matches;
self.set_selected_index(0, cx);
}
}
impl PickerDelegate for ProjectSymbolsDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Search project symbols...".into()
}
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(symbol) = self
.matches
.get(self.selected_match_index)
.map(|mat| self.symbols[mat.candidate_id].clone())
{
let buffer = self.project.update(cx, |project, cx| {
project.open_buffer_for_symbol(&symbol, cx)
});
let symbol = symbol.clone();
let workspace = self.workspace.clone();
cx.spawn(|_, mut cx| async move {
let buffer = buffer.await?;
workspace.update(&mut cx, |workspace, cx| {
let position = buffer
.read(cx)
.clip_point_utf16(symbol.range.start, Bias::Left);
let editor = if secondary {
workspace.split_project_item::<Editor>(buffer, cx)
} else {
workspace.open_project_item::<Editor>(buffer, cx)
};
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
});
});
})?;
Ok::<_, anyhow::Error>(())
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_match_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_match_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
self.filter(&query, cx);
self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
let symbols = self
.project
.update(cx, |project, cx| project.symbols(&query, cx));
cx.spawn(|this, mut cx| async move {
let symbols = symbols.await.log_err();
if let Some(symbols) = symbols {
this.update(&mut cx, |this, cx| {
let delegate = &mut this.delegate;
let project = delegate.project.read(cx);
let (visible_match_candidates, external_match_candidates) = symbols
.iter()
.enumerate()
.map(|(id, symbol)| {
StringMatchCandidate::new(
id,
symbol.label.text[symbol.label.filter_range.clone()].to_string(),
)
})
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
});
delegate.visible_match_candidates = visible_match_candidates;
delegate.external_match_candidates = external_match_candidates;
delegate.symbols = symbols;
delegate.filter(&query, cx);
})
.log_err();
}
})
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let string_match = &self.matches[ix];
let symbol = &self.symbols[string_match.candidate_id];
let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
let mut path = symbol.path.path.to_string_lossy();
if self.show_worktree_root_name {
let project = self.project.read(cx);
if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
path = Cow::Owned(format!(
"{}{}{}",
worktree.read(cx).root_name(),
std::path::MAIN_SEPARATOR,
path.as_ref()
));
}
}
let label = symbol.label.text.clone();
let path = path.to_string().clone();
let highlights = gpui::combine_highlights(
string_match
.positions
.iter()
.map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())),
syntax_runs.map(|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
(range, highlight)
}),
);
Some(
ListItem::new(ix).inset(true).selected(selected).child(
// todo!() combine_syntax_and_fuzzy_match_highlights()
v_stack()
.child(
LabelLike::new().child(
StyledText::new(label)
.with_highlights(&cx.text_style().clone(), highlights),
),
)
.child(Label::new(path).color(Color::Muted)),
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, TestAppContext, VisualContext};
use language::{FakeLspAdapter, Language, LanguageConfig};
use project::FakeFs;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
#[gpui::test]
async fn test_project_symbols(cx: &mut TestAppContext) {
init_test(cx);
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
None,
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
.await;
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/test.rs", cx)
})
.await
.unwrap();
// Set up fake language server to return fuzzy matches against
// a fixed set of symbol names.
let fake_symbols = [
symbol("one", "/external"),
symbol("ton", "/dir/test.rs"),
symbol("uno", "/dir/test.rs"),
];
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
move |params: lsp::WorkspaceSymbolParams, cx| {
let executor = cx.background_executor().clone();
let fake_symbols = fake_symbols.clone();
async move {
let candidates = fake_symbols
.iter()
.enumerate()
.map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
.collect::<Vec<_>>();
let matches = if params.query.is_empty() {
Vec::new()
} else {
fuzzy::match_strings(
&candidates,
&params.query,
true,
100,
&Default::default(),
executor.clone(),
)
.await
};
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
matches
.into_iter()
.map(|mat| fake_symbols[mat.candidate_id].clone())
.collect(),
)))
}
},
);
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
// Create the project symbols view.
let symbols = cx.build_view(|cx| {
Picker::new(
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
cx,
)
});
// Spawn multiples updates before the first update completes,
// such that in the end, there are no matches. Testing for regression:
// https://github.com/zed-industries/zed/issues/861
symbols.update(cx, |p, cx| {
p.update_matches("o".to_string(), cx);
p.update_matches("on".to_string(), cx);
p.update_matches("onex".to_string(), cx);
});
cx.run_until_parked();
symbols.update(cx, |symbols, _| {
assert_eq!(symbols.delegate.matches.len(), 0);
});
// Spawn more updates such that in the end, there are matches.
symbols.update(cx, |p, cx| {
p.update_matches("one".to_string(), cx);
p.update_matches("on".to_string(), cx);
});
cx.run_until_parked();
symbols.update(cx, |symbols, _| {
let delegate = &symbols.delegate;
assert_eq!(delegate.matches.len(), 2);
assert_eq!(delegate.matches[0].string, "ton");
assert_eq!(delegate.matches[1].string, "one");
});
// Spawn more updates such that in the end, there are again no matches.
symbols.update(cx, |p, cx| {
p.update_matches("o".to_string(), cx);
p.update_matches("".to_string(), cx);
});
cx.run_until_parked();
symbols.update(cx, |symbols, _| {
assert_eq!(symbols.delegate.matches.len(), 0);
});
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
});
}
fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
#[allow(deprecated)]
lsp::SymbolInformation {
name: name.to_string(),
kind: lsp::SymbolKind::FUNCTION,
tags: None,
deprecated: None,
container_name: None,
location: lsp::Location::new(
lsp::Url::from_file_path(path.as_ref()).unwrap(),
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
),
}
}
}

View file

@ -23,7 +23,7 @@ use util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
}; };
#[derive(PartialEq, Clone, Deserialize)] #[derive(PartialEq, Clone, Deserialize)]
@ -456,6 +456,9 @@ impl BufferSearchBar {
cx.focus(&handle); cx.focus(&handle);
} }
cx.emit(Event::UpdateLocation); cx.emit(Event::UpdateLocation);
cx.emit(ToolbarItemEvent::ChangeLocation(
ToolbarItemLocation::Hidden,
));
cx.notify(); cx.notify();
} }
@ -488,6 +491,9 @@ impl BufferSearchBar {
self.dismissed = false; self.dismissed = false;
cx.notify(); cx.notify();
cx.emit(Event::UpdateLocation); cx.emit(Event::UpdateLocation);
cx.emit(ToolbarItemEvent::ChangeLocation(
ToolbarItemLocation::Secondary,
));
true true
} }

View file

@ -10,11 +10,13 @@ use editor::{
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent, items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent,
MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
}; };
use editor::{EditorElement, EditorStyle};
use gpui::{ use gpui::{
actions, div, white, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId, actions, div, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
EventEmitter, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, ModelContext, EventEmitter, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement,
ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, View, KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled,
ViewContext, VisualContext, WeakModel, WeakView, WindowContext, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
WhiteSpace, WindowContext,
}; };
use menu::Confirm; use menu::Confirm;
use project::{ use project::{
@ -23,6 +25,7 @@ use project::{
}; };
use semantic_index::{SemanticIndex, SemanticIndexStatus}; use semantic_index::{SemanticIndex, SemanticIndexStatus};
use settings::Settings;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
@ -32,6 +35,7 @@ use std::{
path::PathBuf, path::PathBuf,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use theme::ThemeSettings;
use ui::{ use ui::{
h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon, h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon,
@ -417,7 +421,12 @@ impl Item for ProjectSearchView {
.filter(|query| !query.is_empty()) .filter(|query| !query.is_empty())
.unwrap_or_else(|| "Project search".into()); .unwrap_or_else(|| "Project search".into());
h_stack() h_stack()
.child(IconElement::new(Icon::MagnifyingGlass)) .gap_2()
.child(IconElement::new(Icon::MagnifyingGlass).color(if selected {
Color::Default
} else {
Color::Muted
}))
.child(Label::new(tab_name).color(if selected { .child(Label::new(tab_name).color(if selected {
Color::Default Color::Default
} else { } else {
@ -1420,6 +1429,36 @@ impl ProjectSearchBar {
}; };
new_placeholder_text new_placeholder_text
} }
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if editor.read(cx).read_only() {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3).into(),
background_color: None,
underline: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
} }
impl Render for ProjectSearchBar { impl Render for ProjectSearchBar {
@ -1440,122 +1479,62 @@ impl Render for ProjectSearchBar {
} }
let search = search.read(cx); let search = search.read(cx);
let semantic_is_available = SemanticIndex::enabled(cx); let semantic_is_available = SemanticIndex::enabled(cx);
let query_column = v_stack() let query_column = v_stack().child(
//.flex_1() h_stack()
.child( .min_w(rems(512. / 16.))
h_stack() .px_2()
.min_w_80() .py_1()
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) .gap_2()
.on_action( .bg(cx.theme().colors().editor_background)
cx.listener(|this, action, cx| this.previous_history_query(action, cx)), .border_1()
) .border_color(cx.theme().colors().border)
.on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) .rounded_lg()
.child(IconElement::new(Icon::MagnifyingGlass)) .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
.child(search.query_editor.clone()) .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
.child( .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
h_stack() .child(IconElement::new(Icon::MagnifyingGlass))
.child( .child(self.render_text_input(&search.query_editor, cx))
IconButton::new("project-search-filter-button", Icon::Filter) .child(
.tooltip(|cx| {
Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
})
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
.unwrap_or_default(),
),
)
.when(search.current_mode != SearchMode::Semantic, |this| {
this.child(
IconButton::new(
"project-search-case-sensitive",
Icon::CaseSensitive,
)
.tooltip(|cx| {
Tooltip::for_action(
"Toggle case sensitive",
&ToggleCaseSensitive,
cx,
)
})
.selected(
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
)
.on_click(cx.listener(
|this, _, cx| {
this.toggle_search_option(
SearchOptions::CASE_SENSITIVE,
cx,
);
},
)),
)
.child(
IconButton::new("project-search-whole-word", Icon::WholeWord)
.tooltip(|cx| {
Tooltip::for_action(
"Toggle whole word",
&ToggleWholeWord,
cx,
)
})
.selected(
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
)
.on_click(cx.listener(|this, _, cx| {
this.toggle_search_option(
SearchOptions::WHOLE_WORD,
cx,
);
})),
)
}),
)
.border_2()
.bg(white())
.rounded_lg(),
)
.when(search.filters_enabled, |this| {
this.child(
h_stack() h_stack()
.mt_2()
.flex_1()
.justify_between()
.child( .child(
h_stack() IconButton::new("project-search-filter-button", Icon::Filter)
.flex_1() .tooltip(|cx| {
.border_1() Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
.mr_2() })
.child(search.included_files_editor.clone()) .on_click(cx.listener(|this, _, cx| {
.when(search.current_mode != SearchMode::Semantic, |this| { this.toggle_filters(cx);
this.child( }))
SearchOptions::INCLUDE_IGNORED.as_button( .selected(
search self.active_project_search
.search_options .as_ref()
.contains(SearchOptions::INCLUDE_IGNORED), .map(|search| search.read(cx).filters_enabled)
cx.listener(|this, _, cx| { .unwrap_or_default(),
this.toggle_search_option( ),
SearchOptions::INCLUDE_IGNORED,
cx,
);
}),
),
)
}),
) )
.child( .when(search.current_mode != SearchMode::Semantic, |this| {
h_stack() this.child(
.flex_1() IconButton::new(
.border_1() "project-search-case-sensitive",
.ml_2() Icon::CaseSensitive,
.child(search.excluded_files_editor.clone()), )
), .tooltip(|cx| {
) Tooltip::for_action(
}); "Toggle case sensitive",
&ToggleCaseSensitive,
cx,
)
})
.selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
.on_click(cx.listener(
|this, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
},
)),
)
}),
),
);
let mode_column = v_stack().items_start().justify_start().child( let mode_column = v_stack().items_start().justify_start().child(
h_stack() h_stack()
.child( .child(
@ -1677,11 +1656,11 @@ impl Render for ProjectSearchBar {
})) }))
.tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)), .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
]); ]);
h_stack() v_stack()
.key_context(key_context) .key_context(key_context)
.size_full()
.p_1() .p_1()
.m_2() .m_2()
.gap_2()
.justify_between() .justify_between()
.on_action(cx.listener(|this, _: &ToggleFilters, cx| { .on_action(cx.listener(|this, _: &ToggleFilters, cx| {
this.toggle_filters(cx); this.toggle_filters(cx);

View file

@ -12,9 +12,10 @@ path = "src/storybook2.rs"
anyhow.workspace = true anyhow.workspace = true
# TODO: Remove after diagnosing stack overflow. # TODO: Remove after diagnosing stack overflow.
backtrace-on-stack-overflow = "0.3.0" backtrace-on-stack-overflow = "0.3.0"
clap = { version = "4.4", features = ["derive", "string"] }
editor = { package = "editor2", path = "../editor2" }
chrono = "0.4" chrono = "0.4"
clap = { version = "4.4", features = ["derive", "string"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor = { package = "editor2", path = "../editor2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
itertools = "0.11.0" itertools = "0.11.0"

View file

@ -5,6 +5,7 @@ mod story_selector;
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use dialoguer::FuzzySelect;
use gpui::{ use gpui::{
div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext, div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext,
WindowBounds, WindowOptions, WindowBounds, WindowOptions,
@ -12,11 +13,12 @@ use gpui::{
use log::LevelFilter; use log::LevelFilter;
use settings2::{default_settings, Settings, SettingsStore}; use settings2::{default_settings, Settings, SettingsStore};
use simplelog::SimpleLogger; use simplelog::SimpleLogger;
use strum::IntoEnumIterator;
use theme2::{ThemeRegistry, ThemeSettings}; use theme2::{ThemeRegistry, ThemeSettings};
use ui::prelude::*; use ui::prelude::*;
use crate::assets::Assets; use crate::assets::Assets;
use crate::story_selector::StorySelector; use crate::story_selector::{ComponentStory, StorySelector};
// gpui::actions! { // gpui::actions! {
// storybook, // storybook,
@ -43,7 +45,17 @@ fn main() {
let args = Args::parse(); let args = Args::parse();
let story_selector = args.story.clone(); let story_selector = args.story.clone().unwrap_or_else(|| {
let stories = ComponentStory::iter().collect::<Vec<_>>();
let selection = FuzzySelect::new()
.with_prompt("Choose a story to run:")
.items(&stories)
.interact()
.unwrap();
StorySelector::Component(stories[selection])
});
let theme_name = args.theme.unwrap_or("One Dark".to_string()); let theme_name = args.theme.unwrap_or("One Dark".to_string());
let asset_source = Arc::new(Assets); let asset_source = Arc::new(Assets);
@ -58,7 +70,7 @@ fn main() {
theme2::init(theme2::LoadThemes::All, cx); theme2::init(theme2::LoadThemes::All, cx);
let selector = story_selector.unwrap_or(StorySelector::KitchenSink); let selector = story_selector;
let theme_registry = cx.global::<ThemeRegistry>(); let theme_registry = cx.global::<ThemeRegistry>();
let mut theme_settings = ThemeSettings::get_global(cx).clone(); let mut theme_settings = ThemeSettings::get_global(cx).clone();

View file

@ -1,9 +1,9 @@
use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
use gpui::{ use gpui::{
black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext, black, div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, FontStyle,
FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement,
IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
}; };
@ -133,13 +133,7 @@ impl LayoutRect {
) )
.into(); .into();
cx.paint_quad( cx.paint_quad(fill(Bounds::new(position, size), self.color));
Bounds::new(position, size),
Default::default(),
self.color,
Default::default(),
transparent_black(),
);
} }
} }
@ -775,13 +769,7 @@ impl Element for TerminalElement {
let theme = cx.theme(); let theme = cx.theme();
cx.paint_quad( cx.paint_quad(fill(bounds, layout.background_color));
bounds,
Default::default(),
layout.background_color,
Default::default(),
Hsla::default(),
);
let origin = bounds.origin + Point::new(layout.gutter, px(0.)); let origin = bounds.origin + Point::new(layout.gutter, px(0.));
let terminal_input_handler = TerminalInputHandler { let terminal_input_handler = TerminalInputHandler {

View file

@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::h_stack;
use crate::prelude::*; use crate::prelude::*;
pub trait ButtonCommon: Clickable + Disableable { pub trait ButtonCommon: Clickable + Disableable {
@ -250,6 +249,7 @@ impl ButtonSize {
/// This is also used to build the prebuilt buttons. /// This is also used to build the prebuilt buttons.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ButtonLike { pub struct ButtonLike {
base: Div,
id: ElementId, id: ElementId,
pub(super) style: ButtonStyle, pub(super) style: ButtonStyle,
pub(super) disabled: bool, pub(super) disabled: bool,
@ -264,6 +264,7 @@ pub struct ButtonLike {
impl ButtonLike { impl ButtonLike {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
base: div(),
id: id.into(), id: id.into(),
style: ButtonStyle::default(), style: ButtonStyle::default(),
disabled: false, disabled: false,
@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
} }
} }
impl VisibleOnHover for ButtonLike {
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
self.base = self.base.visible_on_hover(group_name);
self
}
}
impl ParentElement for ButtonLike { impl ParentElement for ButtonLike {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children &mut self.children
@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
type Rendered = Stateful<Div>; type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered { fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack() self.base
.h_flex()
.id(self.id.clone()) .id(self.id.clone())
.group("") .group("")
.flex_none() .flex_none()

View file

@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
} }
} }
impl VisibleOnHover for IconButton {
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
self.base = self.base.visible_on_hover(group_name);
self
}
}
impl RenderOnce for IconButton { impl RenderOnce for IconButton {
type Rendered = ButtonLike; type Rendered = ButtonLike;

View file

@ -255,6 +255,9 @@ impl Render for ContextMenu {
}; };
ListItem::new(label.clone()) ListItem::new(label.clone())
.inset(true)
.selected(Some(ix) == self.selected_index)
.on_click(move |_, cx| handler(cx))
.child( .child(
h_stack() h_stack()
.w_full() .w_full()
@ -265,8 +268,6 @@ impl Render for ContextMenu {
.map(|binding| div().ml_1().child(binding)) .map(|binding| div().ml_1().child(binding))
})), })),
) )
.selected(Some(ix) == self.selected_index)
.on_click(move |_, cx| handler(cx))
.into_any_element() .into_any_element()
} }
}, },

View file

@ -1,6 +1,7 @@
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
use gpui::ClickEvent; use gpui::ClickEvent;
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Disclosure { pub struct Disclosure {
is_open: bool, is_open: bool,

View file

@ -1,12 +1,18 @@
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label}; use crate::{h_stack, prelude::*, Disclosure, Label};
use gpui::{AnyElement, ClickEvent, Div}; use gpui::{AnyElement, ClickEvent, Div};
use smallvec::SmallVec;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ListHeader { pub struct ListHeader {
/// The label of the header.
label: SharedString, label: SharedString,
left_icon: Option<Icon>, /// A slot for content that appears before the label, like an icon or avatar.
meta: SmallVec<[AnyElement; 2]>, start_slot: Option<AnyElement>,
/// A slot for content that appears after the label, usually on the other side of the header.
/// This might be a button, a disclosure arrow, a face pile, etc.
end_slot: Option<AnyElement>,
/// A slot for content that appears on hover after the label
/// It will obscure the `end_slot` when visible.
end_hover_slot: Option<AnyElement>,
toggle: Option<bool>, toggle: Option<bool>,
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
inset: bool, inset: bool,
@ -17,8 +23,9 @@ impl ListHeader {
pub fn new(label: impl Into<SharedString>) -> Self { pub fn new(label: impl Into<SharedString>) -> Self {
Self { Self {
label: label.into(), label: label.into(),
left_icon: None, start_slot: None,
meta: SmallVec::new(), end_slot: None,
end_hover_slot: None,
inset: false, inset: false,
toggle: None, toggle: None,
on_toggle: None, on_toggle: None,
@ -39,13 +46,23 @@ impl ListHeader {
self self
} }
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self { pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
self.left_icon = left_icon.into(); self.start_slot = start_slot.into().map(IntoElement::into_any_element);
self self
} }
pub fn meta(mut self, meta: impl IntoElement) -> Self { pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
self.meta.push(meta.into_any_element()); self.end_slot = end_slot.into().map(IntoElement::into_any_element);
self
}
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
self
}
pub fn inset(mut self, inset: bool) -> Self {
self.inset = inset;
self self
} }
} }
@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
type Rendered = Div; type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered { fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack().w_full().relative().child( h_stack().w_full().relative().group("list_header").child(
div() div()
.h_5() .h_7()
.when(self.inset, |this| this.px_2()) .when(self.inset, |this| this.px_2())
.when(self.selected, |this| { .when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected) this.bg(cx.theme().colors().ghost_element_selected)
@ -77,24 +94,29 @@ impl RenderOnce for ListHeader {
.child( .child(
h_stack() h_stack()
.gap_1() .gap_1()
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
)
.child( .child(
div() div()
.flex() .flex()
.gap_1() .gap_1()
.items_center() .items_center()
.children(self.left_icon.map(|i| { .children(self.start_slot)
IconElement::new(i)
.color(Color::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(Color::Muted)), .child(Label::new(self.label.clone()).color(Color::Muted)),
)
.children(
self.toggle
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
), ),
) )
.child(h_stack().gap_2().items_center().children(self.meta)), .child(h_stack().children(self.end_slot))
.when_some(self.end_hover_slot, |this, end_hover_slot| {
this.child(
div()
.absolute()
.right_0()
.visible_on_hover("list_header")
.child(end_hover_slot),
)
}),
) )
} }
} }

View file

@ -1,19 +1,24 @@
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
use gpui::{ use gpui::{
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
Stateful,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{prelude::*, Disclosure};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ListItem { pub struct ListItem {
id: ElementId, id: ElementId,
selected: bool, selected: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: usize, indent_level: usize,
indent_step_size: Pixels, indent_step_size: Pixels,
left_slot: Option<AnyElement>, /// A slot for content that appears before the children, like an icon or avatar.
start_slot: Option<AnyElement>,
/// A slot for content that appears after the children, usually on the other side of the header.
/// This might be a button, a disclosure arrow, a face pile, etc.
end_slot: Option<AnyElement>,
/// A slot for content that appears on hover after the children
/// It will obscure the `end_slot` when visible.
end_hover_slot: Option<AnyElement>,
toggle: Option<bool>, toggle: Option<bool>,
inset: bool, inset: bool,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>, on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@ -30,7 +35,9 @@ impl ListItem {
selected: false, selected: false,
indent_level: 0, indent_level: 0,
indent_step_size: px(12.), indent_step_size: px(12.),
left_slot: None, start_slot: None,
end_slot: None,
end_hover_slot: None,
toggle: None, toggle: None,
inset: false, inset: false,
on_click: None, on_click: None,
@ -87,23 +94,18 @@ impl ListItem {
self self
} }
pub fn left_child(mut self, left_content: impl IntoElement) -> Self { pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
self.left_slot = Some(left_content.into_any_element()); self.start_slot = start_slot.into().map(IntoElement::into_any_element);
self self
} }
pub fn left_icon(mut self, left_icon: Icon) -> Self { pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
self.left_slot = Some( self.end_slot = end_slot.into().map(IntoElement::into_any_element);
IconElement::new(left_icon)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
);
self self
} }
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self { pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
self.left_slot = Some(Avatar::new(left_avatar).into_any_element()); self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
self self
} }
} }
@ -125,49 +127,102 @@ impl RenderOnce for ListItem {
type Rendered = Stateful<Div>; type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered { fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div() h_stack()
.id(self.id) .id(self.id)
.w_full()
.relative() .relative()
// TODO: Add focus state // When an item is inset draw the indent spacing outside of the item
// .when(self.state == InteractionState::Focused, |this| { .when(self.inset, |this| {
// this.border() this.ml(self.indent_level as f32 * self.indent_step_size)
// .border_color(cx.theme().colors().border_focused) .px_1()
// })
.when(self.inset, |this| this.rounded_md())
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected)
}) })
.when_some(self.on_click, |this, on_click| { .when(!self.inset, |this| {
this.cursor_pointer().on_click(move |event, cx| { this
// HACK: GPUI currently fires `on_click` with any mouse button, // TODO: Add focus state
// but we only care about the left button. // .when(self.state == InteractionState::Focused, |this| {
if event.down.button == MouseButton::Left { // this.border()
(on_click)(event, cx) // .border_color(cx.theme().colors().border_focused)
} // })
}) .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected)
})
}) })
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, cx| {
(on_mouse_down)(event, cx)
})
})
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
.child( .child(
div() h_stack()
.when(self.inset, |this| this.px_2()) .id("inner_list_item")
.ml(self.indent_level as f32 * self.indent_step_size) .w_full()
.flex()
.gap_1()
.items_center()
.relative() .relative()
.children( .gap_1()
self.toggle .px_2()
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), .group("list_item")
.when(self.inset, |this| {
this
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
// .border_color(cx.theme().colors().border_focused)
// })
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.when(self.selected, |this| {
this.bg(cx.theme().colors().ghost_element_selected)
})
})
.when_some(self.on_click, |this, on_click| {
this.cursor_pointer().on_click(on_click)
})
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, cx| {
(on_mouse_down)(event, cx)
})
})
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
.map(|this| {
if self.inset {
this.rounded_md()
} else {
// When an item is not inset draw the indent spacing inside of the item
this.ml(self.indent_level as f32 * self.indent_step_size)
}
})
.children(self.toggle.map(|is_open| {
div()
.flex()
.absolute()
.left(rems(-1.))
.visible_on_hover("")
.child(Disclosure::new(is_open).on_toggle(self.on_toggle))
}))
.child(
h_stack()
.flex_1()
.gap_1()
.children(self.start_slot)
.children(self.children),
) )
.children(self.left_slot) .when_some(self.end_slot, |this, end_slot| {
.children(self.children), this.justify_between().child(
h_stack()
.when(self.end_hover_slot.is_some(), |this| {
this.visible()
.group_hover("list_item", |this| this.invisible())
})
.child(end_slot),
)
})
.when_some(self.end_hover_slot, |this, end_hover_slot| {
this.child(
h_stack()
.h_full()
.absolute()
.right_2()
.top_0()
.visible_on_hover("list_item")
.child(end_hover_slot),
)
}),
) )
} }
} }

View file

@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
type Rendered = Div; type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered { fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div().h_px().w_full().bg(cx.theme().colors().border_variant) div()
.h_px()
.w_full()
.my_1()
.bg(cx.theme().colors().border_variant)
} }
} }

View file

@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct ListSubHeader { pub struct ListSubHeader {
label: SharedString, label: SharedString,
left_icon: Option<Icon>, start_slot: Option<Icon>,
inset: bool, inset: bool,
} }
@ -14,13 +14,13 @@ impl ListSubHeader {
pub fn new(label: impl Into<SharedString>) -> Self { pub fn new(label: impl Into<SharedString>) -> Self {
Self { Self {
label: label.into(), label: label.into(),
left_icon: None, start_slot: None,
inset: false, inset: false,
} }
} }
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self { pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon; self.start_slot = left_icon;
self self
} }
} }
@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
.flex() .flex()
.gap_1() .gap_1()
.items_center() .items_center()
.children(self.left_icon.map(|i| { .children(self.start_slot.map(|i| {
IconElement::new(i) IconElement::new(i)
.color(Color::Muted) .color(Color::Muted)
.size(IconSize::Small) .size(IconSize::Small)

View file

@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
.child(Story::label("Default")) .child(Story::label("Default"))
.child(ListHeader::new("Section 1")) .child(ListHeader::new("Section 1"))
.child(Story::label("With left icon")) .child(Story::label("With left icon"))
.child(ListHeader::new("Section 2").left_icon(Icon::Bell)) .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
.child(Story::label("With left icon and meta")) .child(Story::label("With left icon and meta"))
.child( .child(
ListHeader::new("Section 3") ListHeader::new("Section 3")
.left_icon(Icon::BellOff) .start_slot(IconElement::new(Icon::BellOff))
.meta(IconButton::new("action_1", Icon::Bolt)), .end_slot(IconButton::new("action_1", Icon::Bolt)),
) )
.child(Story::label("With multiple meta")) .child(Story::label("With multiple meta"))
.child( .child(
ListHeader::new("Section 4") ListHeader::new("Section 4")
.meta(IconButton::new("action_1", Icon::Bolt)) .end_slot(IconButton::new("action_1", Icon::Bolt))
.meta(IconButton::new("action_2", Icon::ExclamationTriangle)) .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
.meta(IconButton::new("action_3", Icon::Plus)), .end_slot(IconButton::new("action_3", Icon::Plus)),
) )
} }
} }

View file

@ -1,7 +1,7 @@
use gpui::{Div, Render}; use gpui::{Div, Render};
use story::Story; use story::Story;
use crate::prelude::*; use crate::{prelude::*, Avatar};
use crate::{Icon, ListItem}; use crate::{Icon, ListItem};
pub struct ListItemStory; pub struct ListItemStory;
@ -9,24 +9,80 @@ pub struct ListItemStory;
impl Render for ListItemStory { impl Render for ListItemStory {
type Element = Div; type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container() Story::container()
.bg(cx.theme().colors().background)
.child(Story::title_for::<ListItem>()) .child(Story::title_for::<ListItem>())
.child(Story::label("Default")) .child(Story::label("Default"))
.child(ListItem::new("hello_world").child("Hello, world!")) .child(ListItem::new("hello_world").child("Hello, world!"))
.child(Story::label("With left icon")) .child(Story::label("Inset"))
.child( .child(
ListItem::new("with_left_icon") ListItem::new("inset_list_item")
.inset(true)
.start_slot(
IconElement::new(Icon::Bell)
.size(IconSize::Small)
.color(Color::Muted),
)
.child("Hello, world!") .child("Hello, world!")
.left_icon(Icon::Bell), .end_slot(
IconElement::new(Icon::Bell)
.size(IconSize::Small)
.color(Color::Muted),
),
) )
.child(Story::label("With left avatar")) .child(Story::label("With start slot icon"))
.child(
ListItem::new("with start slot_icon")
.child("Hello, world!")
.start_slot(
IconElement::new(Icon::Bell)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.child(Story::label("With start slot avatar"))
.child(
ListItem::new("with_start slot avatar")
.child("Hello, world!")
.start_slot(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))),
)
.child(Story::label("With end slot"))
.child( .child(
ListItem::new("with_left_avatar") ListItem::new("with_left_avatar")
.child("Hello, world!") .child("Hello, world!")
.left_avatar(SharedString::from( .end_slot(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1714999?v=4", "https://avatars.githubusercontent.com/u/1714999?v=4",
)), ))),
)
.child(Story::label("With end hover slot"))
.child(
ListItem::new("with_end_hover_slot")
.child("Hello, world!")
.end_slot(
h_stack()
.gap_2()
.child(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
.child(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
.child(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
.child(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
)))
.child(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1789?v=4",
))),
)
.end_hover_slot(Avatar::new(SharedString::from(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))),
) )
.child(Story::label("With `on_click`")) .child(Story::label("With `on_click`"))
.child( .child(

View file

@ -158,7 +158,6 @@ impl RenderOnce for Tab {
) )
.child( .child(
h_stack() h_stack()
.invisible()
.w_3() .w_3()
.h_3() .h_3()
.justify_center() .justify_center()
@ -167,7 +166,7 @@ impl RenderOnce for Tab {
TabCloseSide::Start => this.left_1(), TabCloseSide::Start => this.left_1(),
TabCloseSide::End => this.right_1(), TabCloseSide::End => this.right_1(),
}) })
.group_hover("", |style| style.visible()) .visible_on_hover("")
.children(self.end_slot), .children(self.end_slot),
) )
.children(self.children), .children(self.children),

View file

@ -9,6 +9,7 @@ pub use crate::clickable::*;
pub use crate::disableable::*; pub use crate::disableable::*;
pub use crate::fixed::*; pub use crate::fixed::*;
pub use crate::selectable::*; pub use crate::selectable::*;
pub use crate::visible_on_hover::*;
pub use crate::{h_stack, v_stack}; pub use crate::{h_stack, v_stack};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
pub use crate::{ButtonCommon, Color, StyledExt}; pub use crate::{ButtonCommon, Color, StyledExt};

View file

@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
elevated(self, cx, ElevationIndex::ModalSurface) elevated(self, cx, ElevationIndex::ModalSurface)
} }
/// The theme's primary border color.
fn border_primary(self, cx: &mut WindowContext) -> Self {
self.border_color(cx.theme().colors().border)
}
/// The theme's secondary or muted border color.
fn border_muted(self, cx: &mut WindowContext) -> Self {
self.border_color(cx.theme().colors().border_variant)
}
fn debug_bg_red(self) -> Self { fn debug_bg_red(self) -> Self {
self.bg(gpui::red()) self.bg(hsla(0. / 360., 1., 0.5, 1.))
} }
fn debug_bg_green(self) -> Self { fn debug_bg_green(self) -> Self {
self.bg(gpui::green()) self.bg(hsla(120. / 360., 1., 0.5, 1.))
} }
fn debug_bg_blue(self) -> Self { fn debug_bg_blue(self) -> Self {
self.bg(gpui::blue()) self.bg(hsla(240. / 360., 1., 0.5, 1.))
} }
fn debug_bg_yellow(self) -> Self { fn debug_bg_yellow(self) -> Self {

View file

@ -21,6 +21,7 @@ mod selectable;
mod styled_ext; mod styled_ext;
mod styles; mod styles;
pub mod utils; pub mod utils;
mod visible_on_hover;
pub use clickable::*; pub use clickable::*;
pub use components::*; pub use components::*;
@ -30,3 +31,4 @@ pub use prelude::*;
pub use selectable::*; pub use selectable::*;
pub use styled_ext::*; pub use styled_ext::*;
pub use styles::*; pub use styles::*;
pub use visible_on_hover::*;

View file

@ -0,0 +1,15 @@
use gpui::{InteractiveElement, SharedString, Styled};
pub trait VisibleOnHover {
/// Sets the element to only be visible when the specified group is hovered.
///
/// Pass `""` as the `group_name` to use the global group.
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
}
impl<E: InteractiveElement + Styled> VisibleOnHover for E {
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
self.invisible()
.group_hover(group_name, |style| style.visible())
}
}

View file

@ -0,0 +1,17 @@
[package]
name = "vcs_menu2"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
fs = {package = "fs2", path = "../fs2"}
gpui = {package = "gpui2", path = "../gpui2"}
picker = {package = "picker2", path = "../picker2"}
util = {path = "../util"}
ui = {package = "ui2", path = "../ui2"}
workspace = {package = "workspace2", path = "../workspace2"}
anyhow.workspace = true

359
crates/vcs_menu2/src/lib.rs Normal file
View file

@ -0,0 +1,359 @@
use anyhow::{anyhow, bail, Result};
use fs::repository::Branch;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
WindowContext,
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
use util::ResultExt;
use workspace::{ModalView, Toast, Workspace};
actions!(branches, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
// todo!() po
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, action, cx| {
ModalBranchList::toggle_modal(workspace, action, cx).log_err();
});
})
.detach();
}
pub type BranchList = Picker<BranchListDelegate>;
pub struct ModalBranchList {
pub picker: View<Picker<BranchListDelegate>>,
}
impl ModalView for ModalBranchList {}
impl EventEmitter<DismissEvent> for ModalBranchList {}
impl FocusableView for ModalBranchList {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ModalBranchList {
type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w(rems(34.)).child(self.picker.clone())
}
}
pub fn build_branch_list(
workspace: View<Workspace>,
cx: &mut WindowContext<'_>,
) -> Result<View<BranchList>> {
let delegate = workspace.update(cx, |workspace, cx| {
BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
})?;
Ok(cx.build_view(|cx| Picker::new(delegate, cx)))
}
impl ModalBranchList {
fn toggle_modal(
workspace: &mut Workspace,
_: &OpenRecent,
cx: &mut ViewContext<Workspace>,
) -> Result<()> {
// Modal branch picker has a longer trailoff than a popover one.
let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
workspace.toggle_modal(cx, |cx| ModalBranchList {
picker: cx.build_view(|cx| Picker::new(delegate, cx)),
});
Ok(())
}
}
pub struct BranchListDelegate {
matches: Vec<StringMatch>,
all_branches: Vec<Branch>,
workspace: View<Workspace>,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
branch_name_trailoff_after: usize,
}
impl BranchListDelegate {
fn new(
workspace: &Workspace,
handle: View<Workspace>,
branch_name_trailoff_after: usize,
cx: &AppContext,
) -> Result<Self> {
let project = workspace.project().read(&cx);
let Some(worktree) = project.visible_worktrees(cx).next() else {
bail!("Cannot update branch list as there are no visible worktrees")
};
let mut cwd = worktree.read(cx).abs_path().to_path_buf();
cwd.push(".git");
let Some(repo) = project.fs().open_repo(&cwd) else {
bail!("Project does not have associated git repository.")
};
let all_branches = repo.lock().branches()?;
Ok(Self {
matches: vec![],
workspace: handle,
all_branches,
selected_index: 0,
last_query: Default::default(),
branch_name_trailoff_after,
})
}
fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
self.workspace.update(cx, |model, ctx| {
model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
});
}
}
impl PickerDelegate for BranchListDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Select branch...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
cx.spawn(move |picker, mut cx| async move {
let candidates = picker.update(&mut cx, |view, _| {
const RECENT_BRANCHES_COUNT: usize = 10;
let mut branches = view.delegate.all_branches.clone();
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
});
branches.truncate(RECENT_BRANCHES_COUNT);
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
}
branches
.into_iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
char_bag: command.name.chars().collect(),
string: command.name.into(),
})
.collect::<Vec<StringMatchCandidate>>()
});
let Some(candidates) = candidates.log_err() else {
return;
};
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
10000,
&Default::default(),
cx.background_executor().clone(),
)
.await
};
picker
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
}
delegate.last_query = query;
})
.log_err();
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
let current_pick = self.selected_index();
let Some(current_pick) = self
.matches
.get(current_pick)
.map(|pick| pick.string.clone())
else {
return;
};
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |this, cx| {
let project = this.delegate.workspace.read(cx).project().read(cx);
let mut cwd = project
.visible_worktrees(cx)
.next()
.ok_or_else(|| anyhow!("There are no visisible worktrees."))?
.read(cx)
.abs_path()
.to_path_buf();
cwd.push(".git");
let status = project
.fs()
.open_repo(&cwd)
.ok_or_else(|| {
anyhow!(
"Could not open repository at path `{}`",
cwd.as_os_str().to_string_lossy()
)
})?
.lock()
.change_branch(&current_pick);
if status.is_err() {
this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
status?;
}
cx.emit(DismissEvent);
Ok::<(), anyhow::Error>(())
})
.log_err();
})
.detach();
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let shortened_branch_name =
util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
let highlights: Vec<_> = hit
.positions
.iter()
.filter(|index| index < &&self.branch_name_trailoff_after)
.copied()
.collect();
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.start_slot(HighlightedLabel::new(shortened_branch_name, highlights))
.selected(selected),
)
}
// fn render_header(
// &self,
// cx: &mut ViewContext<Picker<Self>>,
// ) -> Option<AnyElement<Picker<Self>>> {
// let theme = &theme::current(cx);
// let style = theme.picker.header.clone();
// let label = if self.last_query.is_empty() {
// Flex::row()
// .with_child(Label::new("Recent branches", style.label.clone()))
// .contained()
// .with_style(style.container)
// } else {
// Flex::row()
// .with_child(Label::new("Branches", style.label.clone()))
// .with_children(self.matches.is_empty().not().then(|| {
// let suffix = if self.matches.len() == 1 { "" } else { "es" };
// Label::new(
// format!("{} match{}", self.matches.len(), suffix),
// style.label,
// )
// .flex_float()
// }))
// .contained()
// .with_style(style.container)
// };
// Some(label.into_any())
// }
// fn render_footer(
// &self,
// cx: &mut ViewContext<Picker<Self>>,
// ) -> Option<AnyElement<Picker<Self>>> {
// if !self.last_query.is_empty() {
// let theme = &theme::current(cx);
// let style = theme.picker.footer.clone();
// enum BranchCreateButton {}
// Some(
// Flex::row().with_child(MouseEventHandler::new::<BranchCreateButton, _>(0, cx, |state, _| {
// let style = style.style_for(state);
// Label::new("Create branch", style.label.clone())
// .contained()
// .with_style(style.container)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_down(MouseButton::Left, |_, _, cx| {
// cx.spawn(|picker, mut cx| async move {
// picker.update(&mut cx, |this, cx| {
// let project = this.delegate().workspace.read(cx).project().read(cx);
// let current_pick = &this.delegate().last_query;
// let mut cwd = project
// .visible_worktrees(cx)
// .next()
// .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
// .read(cx)
// .abs_path()
// .to_path_buf();
// cwd.push(".git");
// let repo = project
// .fs()
// .open_repo(&cwd)
// .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
// let repo = repo
// .lock();
// let status = repo
// .create_branch(&current_pick);
// if status.is_err() {
// this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
// status?;
// }
// let status = repo.change_branch(&current_pick);
// if status.is_err() {
// this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
// status?;
// }
// cx.emit(PickerEvent::Dismiss);
// Ok::<(), anyhow::Error>(())
// })
// }).detach();
// })).aligned().right()
// .into_any(),
// )
// } else {
// None
// }
// }
}

View file

@ -1,5 +1,5 @@
use crate::DraggedDock;
use crate::{status_bar::StatusItemView, Workspace}; use crate::{status_bar::StatusItemView, Workspace};
use crate::{DockClickReset, DockDragState};
use gpui::{ use gpui::{
div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render, EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
@ -493,47 +493,37 @@ impl Render for Dock {
let handler = div() let handler = div()
.id("resize-handle") .id("resize-handle")
.bg(cx.theme().colors().border) .bg(cx.theme().colors().border)
.on_mouse_down(gpui::MouseButton::Left, move |_, cx| { .on_drag(move |cx| cx.build_view(|_| DraggedDock(position)))
cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
})
.on_click(cx.listener(|v, e: &ClickEvent, cx| { .on_click(cx.listener(|v, e: &ClickEvent, cx| {
if e.down.button == MouseButton::Left { if e.down.button == MouseButton::Left && e.down.click_count == 2 {
cx.update_global(|state: &mut DockClickReset, cx| { v.resize_active_panel(None, cx)
if state.0.is_some() {
state.0 = None;
v.resize_active_panel(None, cx)
} else {
let double_click = cx.double_click_interval();
let timer = cx.background_executor().timer(double_click);
state.0 = Some(cx.spawn(|_, mut cx| async move {
timer.await;
cx.update_global(|state: &mut DockClickReset, cx| {
state.0 = None;
})
.ok();
}));
}
})
} }
})); }))
.z_index(1);
const HANDLE_SIZE: Pixels = Pixels(6.);
match self.position() { match self.position() {
DockPosition::Left => { DockPosition::Left => {
post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize()) post_resize_handle =
Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize())
} }
DockPosition::Bottom => { DockPosition::Bottom => {
pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize()) pre_resize_handle =
Some(handler.w_full().min_h(HANDLE_SIZE).cursor_row_resize())
} }
DockPosition::Right => { DockPosition::Right => {
pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize()) pre_resize_handle =
Some(handler.min_w(HANDLE_SIZE).h_full().cursor_col_resize())
} }
} }
div() div()
.flex()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.map(|this| match self.position().axis() { .map(|this| match self.position().axis() {
Axis::Horizontal => this.w(px(size)).h_full(), Axis::Horizontal => this.w(px(size)).h_full().flex_row(),
Axis::Vertical => this.h(px(size)).w_full(), Axis::Vertical => this.h(px(size)).w_full().flex_col(),
}) })
.map(|this| match self.position() { .map(|this| match self.position() {
DockPosition::Left => this.border_r(), DockPosition::Left => this.border_r(),
@ -541,7 +531,14 @@ impl Render for Dock {
DockPosition::Bottom => this.border_t(), DockPosition::Bottom => this.border_t(),
}) })
.children(pre_resize_handle) .children(pre_resize_handle)
.child(entry.panel.to_any()) .child(
div()
.map(|this| match self.position().axis() {
Axis::Horizontal => this.min_w(px(size) - HANDLE_SIZE).h_full(),
Axis::Vertical => this.min_h(px(size) - HANDLE_SIZE).w_full(),
})
.child(entry.panel.to_any()),
)
.children(post_resize_handle) .children(post_resize_handle)
} else { } else {
div() div()

View file

@ -1759,6 +1759,34 @@ impl Pane {
}) })
.log_err(); .log_err();
} }
fn handle_split_tab_drop(
&mut self,
dragged_tab: &View<DraggedTab>,
split_direction: SplitDirection,
cx: &mut ViewContext<'_, Pane>,
) {
let dragged_tab = dragged_tab.read(cx);
let item_id = dragged_tab.item_id;
let from_pane = dragged_tab.pane.clone();
let to_pane = cx.view().clone();
self.workspace
.update(cx, |workspace, cx| {
cx.defer(move |workspace, cx| {
let item = from_pane
.read(cx)
.items()
.find(|item| item.item_id() == item_id)
.map(|item| item.boxed_clone());
if let Some(item) = item {
if let Some(item) = item.clone_on_split(workspace.database_id(), cx) {
workspace.split_item(split_direction, item, cx);
}
}
});
})
.log_err();
}
} }
impl FocusableView for Pane { impl FocusableView for Pane {
@ -1852,7 +1880,54 @@ impl Render for Pane {
.child(self.render_tab_bar(cx)) .child(self.render_tab_bar(cx))
.child(self.toolbar.clone()) .child(self.toolbar.clone())
.child(if let Some(item) = self.active_item() { .child(if let Some(item) = self.active_item() {
div().flex().flex_1().child(item.to_any()) let mut drag_target_color = cx.theme().colors().text;
drag_target_color.a = 0.5;
div()
.flex()
.flex_1()
.relative()
.child(item.to_any())
.child(
div()
.absolute()
.full()
.z_index(1)
.drag_over::<DraggedTab>(|style| style.bg(drag_target_color))
.on_drop(cx.listener(
move |this, dragged_tab: &View<DraggedTab>, cx| {
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
},
)),
)
.children(
[
(SplitDirection::Up, 2),
(SplitDirection::Down, 2),
(SplitDirection::Left, 3),
(SplitDirection::Right, 3),
]
.into_iter()
.map(|(direction, z_index)| {
let div = div()
.absolute()
.z_index(z_index)
.invisible()
.bg(drag_target_color)
.drag_over::<DraggedTab>(|style| style.visible())
.on_drop(cx.listener(
move |this, dragged_tab: &View<DraggedTab>, cx| {
this.handle_split_tab_drop(dragged_tab, direction, cx)
},
));
match direction {
SplitDirection::Up => div.top_0().left_0().right_0().h_32(),
SplitDirection::Down => div.left_0().bottom_0().right_0().h_32(),
SplitDirection::Left => div.top_0().left_0().bottom_0().w_32(),
SplitDirection::Right => div.top_0().bottom_0().right_0().w_32(),
}
}),
)
} else { } else {
h_stack() h_stack()
.items_center() .items_center()

View file

@ -55,6 +55,12 @@ pub struct Toolbar {
} }
impl Toolbar { impl Toolbar {
fn has_any_visible_items(&self) -> bool {
self.items
.iter()
.any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
}
fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> { fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
self.items.iter().filter_map(|(item, location)| { self.items.iter().filter_map(|(item, location)| {
if *location == ToolbarItemLocation::PrimaryLeft { if *location == ToolbarItemLocation::PrimaryLeft {
@ -74,12 +80,28 @@ impl Toolbar {
} }
}) })
} }
fn secondary_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
self.items.iter().filter_map(|(item, location)| {
if *location == ToolbarItemLocation::Secondary {
Some(item.as_ref())
} else {
None
}
})
}
} }
impl Render for Toolbar { impl Render for Toolbar {
type Element = Div; type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
if !self.has_any_visible_items() {
return div();
}
let secondary_item = self.secondary_items().next().map(|item| item.to_any());
v_stack() v_stack()
.border_b() .border_b()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
@ -87,8 +109,10 @@ impl Render for Toolbar {
.child( .child(
h_stack() h_stack()
.justify_between() .justify_between()
.children(self.items.iter().map(|(child, _)| child.to_any())), .child(h_stack().children(self.left_items().map(|item| item.to_any())))
.child(h_stack().children(self.right_items().map(|item| item.to_any()))),
) )
.children(secondary_item)
} }
} }

View file

@ -30,11 +30,11 @@ use futures::{
}; };
use gpui::{ use gpui::{
actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView, actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools; use itertools::Itertools;
@ -227,9 +227,6 @@ pub fn init_settings(cx: &mut AppContext) {
} }
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.default_global::<DockDragState>();
cx.default_global::<DockClickReset>();
init_settings(cx); init_settings(cx);
notifications::init(cx); notifications::init(cx);
@ -466,6 +463,7 @@ pub struct Workspace {
_observe_current_user: Task<Result<()>>, _observe_current_user: Task<Result<()>>,
_schedule_serialize: Option<Task<()>>, _schedule_serialize: Option<Task<()>>,
pane_history_timestamp: Arc<AtomicUsize>, pane_history_timestamp: Arc<AtomicUsize>,
bounds: Bounds<Pixels>,
} }
impl EventEmitter<Event> for Workspace {} impl EventEmitter<Event> for Workspace {}
@ -708,6 +706,8 @@ impl Workspace {
subscriptions, subscriptions,
pane_history_timestamp, pane_history_timestamp,
workspace_actions: Default::default(), workspace_actions: Default::default(),
// This data will be incorrect, but it will be overwritten by the time it needs to be used.
bounds: Default::default(),
} }
} }
@ -1145,7 +1145,7 @@ impl Workspace {
let window = cx.window_handle(); let window = cx.window_handle();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let workspace_count = cx.update(|_, cx| { let workspace_count = (*cx).update(|cx| {
cx.windows() cx.windows()
.iter() .iter()
.filter(|window| window.downcast::<Workspace>().is_some()) .filter(|window| window.downcast::<Workspace>().is_some())
@ -3580,13 +3580,8 @@ impl FocusableView for Workspace {
struct WorkspaceBounds(Bounds<Pixels>); struct WorkspaceBounds(Bounds<Pixels>);
//todo!("remove this when better drag APIs are in GPUI2") #[derive(Render)]
#[derive(Default)] struct DraggedDock(DockPosition);
struct DockDragState(Option<DockPosition>);
//todo!("remove this when better double APIs are in GPUI2")
#[derive(Default)]
struct DockClickReset(Option<Task<()>>);
impl Render for Workspace { impl Render for Workspace {
type Element = Div; type Element = Div;
@ -3632,37 +3627,37 @@ impl Render for Workspace {
.border_t() .border_t()
.border_b() .border_b()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.on_mouse_up(gpui::MouseButton::Left, |_, cx| { .child(
cx.update_global(|drag: &mut DockDragState, cx| { canvas(cx.listener(|workspace, bounds, cx| {
drag.0 = None; workspace.bounds = *bounds;
}) }))
}) .absolute()
.on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| { .size_full(),
if let Some(types) = &cx.global::<DockDragState>().0 { )
let workspace_bounds = cx.global::<WorkspaceBounds>().0; .on_drag_move(
match types { cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
match e.drag.read(cx).0 {
DockPosition::Left => { DockPosition::Left => {
let size = e.position.x; let size = workspace.bounds.left() + e.event.position.x;
workspace.left_dock.update(cx, |left_dock, cx| { workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size.0), cx); left_dock.resize_active_panel(Some(size.0), cx);
}); });
} }
DockPosition::Right => { DockPosition::Right => {
let size = workspace_bounds.size.width - e.position.x; let size = workspace.bounds.right() - e.event.position.x;
workspace.right_dock.update(cx, |right_dock, cx| { workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size.0), cx); right_dock.resize_active_panel(Some(size.0), cx);
}); });
} }
DockPosition::Bottom => { DockPosition::Bottom => {
let size = workspace_bounds.size.height - e.position.y; let size = workspace.bounds.bottom() - e.event.position.y;
workspace.bottom_dock.update(cx, |bottom_dock, cx| { workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size.0), cx); bottom_dock.resize_active_panel(Some(size.0), cx);
}); });
} }
} }
} }),
})) )
.child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
.child(self.modal_layer.clone()) .child(self.modal_layer.clone())
.child( .child(
div() div()

View file

@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" }
# plugin_runtime = { path = "../plugin_runtime",optional = true } # plugin_runtime = { path = "../plugin_runtime",optional = true }
project = { package = "project2", path = "../project2" } project = { package = "project2", path = "../project2" }
project_panel = { package = "project_panel2", path = "../project_panel2" } project_panel = { package = "project_panel2", path = "../project_panel2" }
# project_symbols = { path = "../project_symbols" } project_symbols = { package = "project_symbols2", path = "../project_symbols2" }
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
recent_projects = { package = "recent_projects2", path = "../recent_projects2" } recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
rope = { package = "rope2", path = "../rope2"} rope = { package = "rope2", path = "../rope2"}

View file

@ -205,7 +205,7 @@ fn main() {
go_to_line::init(cx); go_to_line::init(cx);
file_finder::init(cx); file_finder::init(cx);
outline::init(cx); outline::init(cx);
// project_symbols::init(cx); project_symbols::init(cx);
project_panel::init(Assets, cx); project_panel::init(Assets, cx);
channel::init(&client, user_store.clone(), cx); channel::init(&client, user_store.clone(), cx);
// diagnostics::init(cx); // diagnostics::init(cx);

View file

@ -427,7 +427,7 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
toolbar.add_item(quick_action_bar, cx); toolbar.add_item(quick_action_bar, cx);
let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new());
// toolbar.add_item(diagnostic_editor_controls, cx); toolbar.add_item(diagnostic_editor_controls, cx);
let project_search_bar = cx.build_view(|_| ProjectSearchBar::new()); let project_search_bar = cx.build_view(|_| ProjectSearchBar::new());
toolbar.add_item(project_search_bar, cx); toolbar.add_item(project_search_bar, cx);
// let lsp_log_item = // let lsp_log_item =

1
docker-compose.sql Normal file
View file

@ -0,0 +1 @@
create database zed;

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
version: "3.7"
services:
postgres:
image: postgres:15
container_name: zed_postgres
ports:
- 5432:5432
environment:
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
postgres_data:

7
script/storybook Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z "$1" ]; then
cargo run -p storybook2
else
cargo run -p storybook2 -- "components/$1"
fi